mcp-mesh 0.5.7__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 (39) 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 +91 -18
  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 +229 -99
  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 +9 -72
  26. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +6 -3
  27. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +222 -0
  28. _mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +7 -0
  29. _mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +65 -4
  30. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +2 -2
  31. _mcp_mesh/shared/registry_client_wrapper.py +60 -4
  32. _mcp_mesh/utils/fastmcp_schema_extractor.py +476 -0
  33. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/METADATA +1 -1
  34. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/RECORD +39 -25
  35. mesh/__init__.py +8 -4
  36. mesh/decorators.py +344 -2
  37. mesh/types.py +145 -94
  38. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/WHEEL +0 -0
  39. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -14,7 +14,7 @@ import weakref
14
14
  from collections.abc import Callable
15
15
  from typing import Any
16
16
 
17
- from .signature_analyzer import get_agent_parameter_types, get_mesh_agent_positions
17
+ from .signature_analyzer import get_mesh_agent_positions, has_llm_agent_parameter
18
18
 
19
19
  logger = logging.getLogger(__name__)
20
20
 
@@ -57,17 +57,17 @@ def analyze_injection_strategy(func: Callable, dependencies: list[str]) -> list[
57
57
  logger.warning(
58
58
  f"Single parameter '{param_name}' in function '{func_name}' found, "
59
59
  f"injecting {dependencies[0] if dependencies else 'dependency'} proxy "
60
- f"(consider typing as McpMeshAgent or McpAgent for clarity)"
60
+ f"(consider typing as McpMeshAgent for clarity)"
61
61
  )
62
62
  return [0] # Inject into the single parameter
63
63
 
64
- # Multiple parameters rule: only inject into McpMeshAgent or McpAgent typed parameters
64
+ # Multiple parameters rule: only inject into McpMeshAgent typed parameters
65
65
  if param_count > 1:
66
66
  if not mesh_positions:
67
67
  logger.warning(
68
68
  f"⚠️ Function '{func_name}' has {param_count} parameters but none are "
69
- f"typed as McpMeshAgent or McpAgent. Skipping injection of {len(dependencies)} dependencies. "
70
- f"Consider typing dependency parameters as McpMeshAgent or McpAgent."
69
+ f"typed as McpMeshAgent. Skipping injection of {len(dependencies)} dependencies. "
70
+ f"Consider typing dependency parameters as McpMeshAgent."
71
71
  )
72
72
  return []
73
73
 
@@ -77,7 +77,7 @@ def analyze_injection_strategy(func: Callable, dependencies: list[str]) -> list[
77
77
  excess_deps = dependencies[len(mesh_positions) :]
78
78
  logger.warning(
79
79
  f"Function '{func_name}' has {len(dependencies)} dependencies "
80
- f"but only {len(mesh_positions)} McpMeshAgent/McpAgent parameters. "
80
+ f"but only {len(mesh_positions)} McpMeshAgent parameters. "
81
81
  f"Dependencies {excess_deps} will not be injected."
82
82
  )
83
83
  else:
@@ -85,7 +85,7 @@ def analyze_injection_strategy(func: Callable, dependencies: list[str]) -> list[
85
85
  params[pos].name for pos in mesh_positions[len(dependencies) :]
86
86
  ]
87
87
  logger.warning(
88
- f"Function '{func_name}' has {len(mesh_positions)} McpMeshAgent/McpAgent parameters "
88
+ f"Function '{func_name}' has {len(mesh_positions)} McpMeshAgent parameters "
89
89
  f"but only {len(dependencies)} dependencies declared. "
90
90
  f"Parameters {excess_params} will remain None."
91
91
  )
@@ -101,10 +101,11 @@ class DependencyInjector:
101
101
  Manages dynamic dependency injection for mesh agents.
102
102
 
103
103
  This class:
104
- 1. Maintains a registry of available dependencies
105
- 2. Tracks which functions depend on which services
106
- 3. Updates function bindings when topology changes
107
- 4. Handles graceful degradation when dependencies unavailable
104
+ 1. Maintains a registry of available dependencies (McpMeshAgent)
105
+ 2. Coordinates with MeshLlmAgentInjector for LLM agent injection
106
+ 3. Tracks which functions depend on which services
107
+ 4. Updates function bindings when topology changes
108
+ 5. Handles graceful degradation when dependencies unavailable
108
109
  """
109
110
 
110
111
  def __init__(self):
@@ -117,6 +118,12 @@ class DependencyInjector:
117
118
  ) # dep_name -> set of function_ids
118
119
  self._lock = asyncio.Lock()
119
120
 
121
+ # LLM agent injector for MeshLlmAgent parameters
122
+ from .mesh_llm_agent_injector import get_global_llm_injector
123
+
124
+ self._llm_injector = get_global_llm_injector()
125
+ logger.debug("🤖 DependencyInjector initialized with MeshLlmAgentInjector")
126
+
120
127
  async def register_dependency(self, name: str, instance: Any) -> None:
121
128
  """Register a new dependency or update existing one.
122
129
 
@@ -282,6 +289,49 @@ class DependencyInjector:
282
289
  )
283
290
  return None
284
291
 
292
+ def process_llm_tools(self, llm_tools: dict[str, list[dict[str, Any]]]) -> None:
293
+ """
294
+ Process llm_tools from registry response and delegate to MeshLlmAgentInjector.
295
+
296
+ Args:
297
+ llm_tools: Dict mapping function_id -> list of tool metadata
298
+ Format: {"function_id": [{"function_name": "...", "endpoint": {...}, ...}]}
299
+ """
300
+ logger.info(
301
+ f"🤖 DependencyInjector processing llm_tools for {len(llm_tools)} functions"
302
+ )
303
+ self._llm_injector.process_llm_tools(llm_tools)
304
+
305
+ def update_llm_tools(self, llm_tools: dict[str, list[dict[str, Any]]]) -> None:
306
+ """
307
+ Update llm_tools when topology changes (heartbeat updates).
308
+
309
+ Args:
310
+ llm_tools: Updated llm_tools dict from registry
311
+ """
312
+ logger.info(
313
+ f"🔄 DependencyInjector updating llm_tools for {len(llm_tools)} functions"
314
+ )
315
+ self._llm_injector.update_llm_tools(llm_tools)
316
+
317
+ def create_llm_injection_wrapper(
318
+ self, func: Callable, function_id: str
319
+ ) -> Callable:
320
+ """
321
+ Create wrapper for function with MeshLlmAgent parameter.
322
+
323
+ Delegates to MeshLlmAgentInjector.
324
+
325
+ Args:
326
+ func: Function to wrap
327
+ function_id: Unique function ID from @mesh.llm decorator
328
+
329
+ Returns:
330
+ Wrapped function with MeshLlmAgent injection
331
+ """
332
+ logger.debug(f"🤖 Creating LLM injection wrapper for {function_id}")
333
+ return self._llm_injector.create_injection_wrapper(func, function_id)
334
+
285
335
  def create_injection_wrapper(
286
336
  self, func: Callable, dependencies: list[str]
287
337
  ) -> Callable:
@@ -300,9 +350,6 @@ class DependencyInjector:
300
350
  # Use new smart injection strategy
301
351
  mesh_positions = analyze_injection_strategy(func, dependencies)
302
352
 
303
- # Get parameter type information for proxy selection
304
- parameter_types = get_agent_parameter_types(func)
305
-
306
353
  # Track which dependencies this function needs (using composite keys)
307
354
  for dep_index, dep in enumerate(dependencies):
308
355
  dep_key = f"{func_id}:dep_{dep_index}"
@@ -365,7 +412,6 @@ class DependencyInjector:
365
412
  minimal_wrapper._mesh_injected_deps = [None] * len(dependencies)
366
413
  minimal_wrapper._mesh_dependencies = dependencies
367
414
  minimal_wrapper._mesh_positions = mesh_positions
368
- minimal_wrapper._mesh_parameter_types = get_agent_parameter_types(func)
369
415
  minimal_wrapper._mesh_original_func = func
370
416
 
371
417
  def update_dependency(dep_index: int, instance: Any | None) -> None:
@@ -463,6 +509,21 @@ class DependencyInjector:
463
509
  f"🔧 DEPENDENCY_WRAPPER: final_kwargs={final_kwargs}"
464
510
  )
465
511
 
512
+ # ===== INJECT LLM AGENT IF PRESENT (Option A) =====
513
+ # Check if this function has @mesh.llm metadata attached (on the original function)
514
+ if hasattr(func, "_mesh_llm_param_name"):
515
+ llm_param = func._mesh_llm_param_name
516
+ # Only inject if not already provided
517
+ if (
518
+ llm_param not in final_kwargs
519
+ or final_kwargs.get(llm_param) is None
520
+ ):
521
+ llm_agent = getattr(func, "_mesh_llm_agent", None)
522
+ final_kwargs[llm_param] = llm_agent
523
+ wrapper_logger.debug(
524
+ f"🤖 LLM_INJECTION: Injected {llm_param}={llm_agent}"
525
+ )
526
+
466
527
  # ===== EXECUTE WITH DEPENDENCY INJECTION AND TRACING =====
467
528
  # Use ExecutionTracer for comprehensive execution logging (v0.4.0 style)
468
529
  from ..tracing.execution_tracer import ExecutionTracer
@@ -540,6 +601,21 @@ class DependencyInjector:
540
601
  final_kwargs[param_name] = dependency
541
602
  injected_count += 1
542
603
 
604
+ # ===== INJECT LLM AGENT IF PRESENT (Option A) =====
605
+ # Check if this function has @mesh.llm metadata attached (on the original function)
606
+ if hasattr(func, "_mesh_llm_param_name"):
607
+ llm_param = func._mesh_llm_param_name
608
+ # Only inject if not already provided
609
+ if (
610
+ llm_param not in final_kwargs
611
+ or final_kwargs.get(llm_param) is None
612
+ ):
613
+ llm_agent = getattr(func, "_mesh_llm_agent", None)
614
+ final_kwargs[llm_param] = llm_agent
615
+ wrapper_logger.debug(
616
+ f"🤖 LLM_INJECTION: Injected {llm_param}={llm_agent}"
617
+ )
618
+
543
619
  # ===== EXECUTE WITH DEPENDENCY INJECTION AND TRACING =====
544
620
  # Use ExecutionTracer for comprehensive execution logging (v0.4.0 style)
545
621
  from ..tracing.execution_tracer import ExecutionTracer
@@ -587,9 +663,6 @@ class DependencyInjector:
587
663
  dependency_wrapper._mesh_update_dependency = update_dependency
588
664
  dependency_wrapper._mesh_dependencies = dependencies
589
665
  dependency_wrapper._mesh_positions = mesh_positions
590
- dependency_wrapper._mesh_parameter_types = (
591
- parameter_types # Store for proxy selection
592
- )
593
666
  dependency_wrapper._mesh_original_func = func
594
667
 
595
668
  # Register this wrapper for dependency updates
@@ -170,32 +170,12 @@ class HttpMcpWrapper:
170
170
  async def setup(self):
171
171
  """Set up FastMCP app for integration (no separate wrapper app)."""
172
172
 
173
- # Debug the FastMCP server instance first
174
- logger.debug(f"🔍 DEBUG: FastMCP server type: {type(self.mcp_server)}")
175
- logger.debug(
176
- f"🔍 DEBUG: FastMCP server module: {type(self.mcp_server).__module__}"
177
- )
178
-
179
173
  # Using FastMCP library (fastmcp>=2.8.0)
180
174
  logger.info(
181
175
  "🆕 HTTP Wrapper: Server instance is from FastMCP library (fastmcp)"
182
176
  )
183
177
 
184
- logger.debug(
185
- f"🔍 DEBUG: FastMCP server dir: {[attr for attr in dir(self.mcp_server) if 'app' in attr.lower()]}"
186
- )
187
- logger.debug(f"🔍 DEBUG: Has http_app: {hasattr(self.mcp_server, 'http_app')}")
188
-
189
178
  if self._mcp_app is not None:
190
- logger.debug("🔍 DEBUG: FastMCP app prepared for integration")
191
- logger.debug(f"🔍 DEBUG: FastMCP app type: {type(self._mcp_app)}")
192
-
193
- # Debug: Check what routes the FastMCP app has
194
- if hasattr(self._mcp_app, "routes"):
195
- logger.debug(
196
- f"🔍 DEBUG: FastMCP app routes: {[route.path for route in self._mcp_app.routes if hasattr(route, 'path')]}"
197
- )
198
-
199
179
  # Phase 5: Add session routing middleware to FastMCP app
200
180
  self._add_session_routing_middleware()
201
181
 
@@ -396,10 +376,11 @@ class HttpMcpWrapper:
396
376
 
397
377
  class MCPSessionRoutingMiddleware(BaseHTTPMiddleware):
398
378
  """Clean session routing middleware for MCP requests (v0.4.0 style).
399
-
379
+
400
380
  Handles session affinity and basic trace context setup only.
401
381
  Function execution tracing is handled by ExecutionTracer in DependencyInjector.
402
382
  """
383
+
403
384
  def __init__(self, app, http_wrapper):
404
385
  super().__init__(app)
405
386
  self.http_wrapper = http_wrapper
@@ -455,7 +436,9 @@ class HttpMcpWrapper:
455
436
 
456
437
  # Add the middleware to FastMCP app
457
438
  self._mcp_app.add_middleware(MCPSessionRoutingMiddleware, http_wrapper=self)
458
- logger.info("✅ Clean session routing middleware added to FastMCP app (v0.4.0 style)")
439
+ logger.info(
440
+ "✅ Clean session routing middleware added to FastMCP app (v0.4.0 style)"
441
+ )
459
442
 
460
443
  async def _extract_session_id(self, request) -> str:
461
444
  """Extract session ID from request headers or body."""
@@ -0,0 +1,41 @@
1
+ """
2
+ LLM configuration dataclass.
3
+
4
+ Consolidates LLM-related configuration into a single type-safe structure.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Optional
9
+
10
+
11
+ @dataclass
12
+ class LLMConfig:
13
+ """
14
+ Configuration for MeshLlmAgent.
15
+
16
+ Consolidates provider, model, and runtime settings into a single type-safe structure.
17
+ """
18
+
19
+ provider: str = "claude"
20
+ """LLM provider (e.g., 'claude', 'openai', 'gemini')"""
21
+
22
+ model: str = "claude-3-5-sonnet-20241022"
23
+ """Model name for the provider"""
24
+
25
+ api_key: str = ""
26
+ """API key for the provider (uses environment variable if empty)"""
27
+
28
+ max_iterations: int = 10
29
+ """Maximum iterations for the agentic loop"""
30
+
31
+ system_prompt: Optional[str] = None
32
+ """Optional system prompt to prepend to all interactions"""
33
+
34
+ def __post_init__(self):
35
+ """Validate configuration after initialization."""
36
+ if self.max_iterations < 1:
37
+ raise ValueError("max_iterations must be >= 1")
38
+ if not self.provider:
39
+ raise ValueError("provider cannot be empty")
40
+ if not self.model:
41
+ raise ValueError("model cannot be empty")
@@ -0,0 +1,115 @@
1
+ """
2
+ Enhanced error classes for LLM engine with structured context.
3
+
4
+ Provides rich debugging information for better troubleshooting and telemetry.
5
+ """
6
+
7
+ from typing import Any, Optional
8
+
9
+
10
+ class MaxIterationsError(Exception):
11
+ """
12
+ Raised when max iterations are exceeded in agentic loop.
13
+
14
+ Attributes:
15
+ iteration_count: Number of iterations that were attempted
16
+ max_allowed: Maximum iterations allowed
17
+ function_id: Optional function ID for debugging
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ iteration_count: int,
23
+ max_allowed: int,
24
+ function_id: Optional[str] = None,
25
+ ):
26
+ self.iteration_count = iteration_count
27
+ self.max_allowed = max_allowed
28
+ self.function_id = function_id
29
+
30
+ message = (
31
+ f"Exceeded maximum {max_allowed} iterations without reaching final response"
32
+ )
33
+ if function_id:
34
+ message += f" (function_id={function_id})"
35
+ super().__init__(message)
36
+
37
+
38
+ class LLMAPIError(Exception):
39
+ """
40
+ Raised when LLM API call fails.
41
+
42
+ Attributes:
43
+ provider: LLM provider name (e.g., 'claude', 'openai')
44
+ model: Model name
45
+ original_error: The underlying exception
46
+ status_code: HTTP status code if available
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ provider: str,
52
+ model: str,
53
+ original_error: Exception,
54
+ status_code: Optional[int] = None,
55
+ ):
56
+ self.provider = provider
57
+ self.model = model
58
+ self.original_error = original_error
59
+ self.status_code = status_code
60
+
61
+ message = f"LLM API call failed: {original_error}"
62
+ if status_code:
63
+ message = f"LLM API call failed (HTTP {status_code}): {original_error}"
64
+ message += f" [provider={provider}, model={model}]"
65
+ super().__init__(message)
66
+
67
+
68
+ class ToolExecutionError(Exception):
69
+ """
70
+ Raised when a tool execution fails.
71
+
72
+ Attributes:
73
+ tool_name: Name of the tool that failed
74
+ arguments: Arguments passed to the tool
75
+ original_error: The underlying exception
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ tool_name: str,
81
+ arguments: dict[str, Any],
82
+ original_error: Exception,
83
+ ):
84
+ self.tool_name = tool_name
85
+ self.arguments = arguments
86
+ self.original_error = original_error
87
+
88
+ message = f"Tool '{tool_name}' execution failed: {original_error}"
89
+ super().__init__(message)
90
+
91
+
92
+ class ResponseParseError(Exception):
93
+ """
94
+ Raised when response parsing or validation fails.
95
+
96
+ Attributes:
97
+ raw_content: Raw response content (truncated to 500 chars)
98
+ expected_schema: Expected Pydantic schema name
99
+ validation_errors: Pydantic validation errors if available
100
+ """
101
+
102
+ def __init__(
103
+ self,
104
+ raw_content: str,
105
+ expected_schema: str,
106
+ validation_errors: Optional[str] = None,
107
+ ):
108
+ self.raw_content = raw_content[:500] # Truncate for logging
109
+ self.expected_schema = expected_schema
110
+ self.validation_errors = validation_errors
111
+
112
+ message = f"Response validation failed for schema '{expected_schema}'"
113
+ if validation_errors:
114
+ message += f": {validation_errors}"
115
+ super().__init__(message)