mcp-mesh 0.5.0__py3-none-any.whl → 0.5.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.
_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.0"
34
+ __version__ = "0.5.2"
35
35
 
36
36
  # Store reference to runtime processor if initialized
37
37
  _runtime_processor = None
@@ -17,12 +17,15 @@ __all__ = [
17
17
  # Dependency injection
18
18
  "DependencyInjector",
19
19
  "get_global_injector",
20
- # MCP client proxies
20
+ # MCP client proxies (legacy)
21
21
  "MCPClientProxy",
22
22
  "EnhancedMCPClientProxy",
23
23
  "FullMCPProxy",
24
24
  "EnhancedFullMCPProxy",
25
25
  "AsyncMCPClient",
26
+ # Unified MCP proxy (recommended)
27
+ "UnifiedMCPProxy",
28
+ "EnhancedUnifiedMCPProxy",
26
29
  # Self-dependency proxy
27
30
  "SelfDependencyProxy",
28
31
  # Decorator registry
@@ -76,6 +79,14 @@ def __getattr__(name):
76
79
  from .async_mcp_client import AsyncMCPClient
77
80
 
78
81
  return AsyncMCPClient
82
+ elif name == "UnifiedMCPProxy":
83
+ from .unified_mcp_proxy import UnifiedMCPProxy
84
+
85
+ return UnifiedMCPProxy
86
+ elif name == "EnhancedUnifiedMCPProxy":
87
+ from .unified_mcp_proxy import EnhancedUnifiedMCPProxy
88
+
89
+ return EnhancedUnifiedMCPProxy
79
90
  # Self-dependency proxy
80
91
  elif name == "SelfDependencyProxy":
81
92
  from .self_dependency_proxy import SelfDependencyProxy
@@ -39,7 +39,7 @@ class AsyncMCPClient:
39
39
 
40
40
  async def _make_request(self, payload: dict) -> dict:
41
41
  """Make async HTTP request to MCP endpoint."""
42
- url = f"{self.endpoint}/mcp/"
42
+ url = f"{self.endpoint}/mcp"
43
43
 
44
44
  try:
45
45
  # Use httpx for proper async HTTP requests (better threading support than aiohttp)
@@ -91,7 +91,7 @@ class AsyncMCPClient:
91
91
 
92
92
  async def _make_request_sync(self, payload: dict) -> dict:
93
93
  """Fallback sync HTTP request using urllib."""
94
- url = f"{self.endpoint}/mcp/"
94
+ url = f"{self.endpoint}/mcp"
95
95
  data = json.dumps(payload).encode("utf-8")
96
96
 
97
97
  # Create request
@@ -277,6 +277,28 @@ class DecoratorRegistry:
277
277
  # Cache for resolved agent configuration to avoid repeated work
278
278
  _cached_agent_config: Optional[dict[str, Any]] = None
279
279
 
280
+ @classmethod
281
+ def update_agent_config(cls, updates: dict[str, Any]) -> None:
282
+ """
283
+ Update the cached agent configuration with new values.
284
+
285
+ This is useful for API services that generate their agent ID
286
+ during pipeline execution and need to store it for telemetry.
287
+
288
+ Args:
289
+ updates: Dictionary of config values to update
290
+ """
291
+ if cls._cached_agent_config is None:
292
+ # Initialize with current resolved config if not cached yet
293
+ cls._cached_agent_config = cls.get_resolved_agent_config().copy()
294
+
295
+ # Update with new values
296
+ cls._cached_agent_config.update(updates)
297
+
298
+ logger.debug(
299
+ f"🔧 Updated cached agent configuration with: {updates}"
300
+ )
301
+
280
302
  @classmethod
281
303
  def get_resolved_agent_config(cls) -> dict[str, Any]:
282
304
  """
@@ -288,11 +310,14 @@ class DecoratorRegistry:
288
310
  Returns:
289
311
  dict: Pre-resolved configuration with consistent agent_id
290
312
  """
291
- # Return cached configuration if available
292
- if cls._cached_agent_config is not None:
313
+ # Step 1: Check if cached configuration already has agent_id (from API pipeline)
314
+ if cls._cached_agent_config is not None and cls._cached_agent_config.get('agent_id'):
315
+ logger.debug(
316
+ f"🔧 Using cached agent configuration: agent_id='{cls._cached_agent_config.get('agent_id')}'"
317
+ )
293
318
  return cls._cached_agent_config
294
319
 
295
- # If we have explicit @mesh.agent configuration, use it
320
+ # Step 2: If we have explicit @mesh.agent configuration, use it
296
321
  if cls._mesh_agents:
297
322
  for agent_name, decorated_func in cls._mesh_agents.items():
298
323
  # Return the already-resolved configuration from decorator
@@ -306,14 +331,23 @@ class DecoratorRegistry:
306
331
  )
307
332
  return resolved_config
308
333
 
309
- # Fallback: Synthetic defaults when no @mesh.agent decorator exists
310
- # This happens when only @mesh.tool decorators are used
311
- from mesh.decorators import _get_or_create_agent_id
312
-
334
+ # Step 3: Fallback to synthetic defaults when no @mesh.agent decorator exists
335
+ # This happens when only @mesh.tool decorators are used and no cached agent_id
313
336
  from ..shared.config_resolver import ValidationRule, get_config_value
314
337
  from ..shared.defaults import MeshDefaults
315
338
 
316
- agent_id = _get_or_create_agent_id()
339
+ # Check if we're in an API context (have mesh_route decorators)
340
+ mesh_routes = cls.get_all_by_type("mesh_route")
341
+ is_api_context = len(mesh_routes) > 0
342
+
343
+ if is_api_context:
344
+ # Use API service ID generation logic for consistency
345
+ agent_id = cls._generate_api_service_id_fallback()
346
+ else:
347
+ # Use standard MCP agent ID generation
348
+ from mesh.decorators import _get_or_create_agent_id
349
+ agent_id = _get_or_create_agent_id()
350
+
317
351
  fallback_config = {
318
352
  "name": None,
319
353
  "version": get_config_value(
@@ -368,6 +402,62 @@ class DecoratorRegistry:
368
402
  )
369
403
  return fallback_config
370
404
 
405
+ @classmethod
406
+ def _generate_api_service_id_fallback(cls) -> str:
407
+ """
408
+ Generate API service ID as fallback using same priority logic as API pipeline.
409
+
410
+ Priority order:
411
+ 1. MCP_MESH_API_NAME environment variable
412
+ 2. MCP_MESH_AGENT_NAME environment variable (fallback)
413
+ 3. Default to "api-{uuid8}"
414
+
415
+ Returns:
416
+ Generated service ID with UUID suffix matching API service format
417
+ """
418
+ import uuid
419
+
420
+ from ..shared.config_resolver import ValidationRule, get_config_value
421
+
422
+ # Check for API-specific environment variable first (same as API pipeline)
423
+ api_name = get_config_value(
424
+ "MCP_MESH_API_NAME",
425
+ default=None,
426
+ rule=ValidationRule.STRING_RULE,
427
+ )
428
+
429
+ # Fallback to general agent name env var
430
+ if not api_name:
431
+ api_name = get_config_value(
432
+ "MCP_MESH_AGENT_NAME",
433
+ default=None,
434
+ rule=ValidationRule.STRING_RULE,
435
+ )
436
+
437
+ # Clean the service name if provided
438
+ if api_name:
439
+ cleaned_name = api_name.lower().replace(" ", "-").replace("_", "-")
440
+ cleaned_name = "-".join(part for part in cleaned_name.split("-") if part)
441
+ else:
442
+ cleaned_name = ""
443
+
444
+ # Generate UUID suffix
445
+ uuid_suffix = str(uuid.uuid4())[:8]
446
+
447
+ # Apply same naming logic as API pipeline
448
+ if not cleaned_name:
449
+ # No name provided: default to "api-{uuid8}"
450
+ service_id = f"api-{uuid_suffix}"
451
+ elif "api" in cleaned_name.lower():
452
+ # Name already contains "api": use "{name}-{uuid8}"
453
+ service_id = f"{cleaned_name}-{uuid_suffix}"
454
+ else:
455
+ # Name doesn't contain "api": use "{name}-api-{uuid8}"
456
+ service_id = f"{cleaned_name}-api-{uuid_suffix}"
457
+
458
+ logger.debug(f"Generated fallback API service ID: '{service_id}' from env name: '{api_name}'")
459
+ return service_id
460
+
371
461
  @classmethod
372
462
  def get_all_agents(cls) -> list[tuple[Any, dict[str, Any]]]:
373
463
  """
@@ -2,6 +2,8 @@
2
2
  Dynamic dependency injection system for MCP Mesh.
3
3
 
4
4
  Handles both initial injection and runtime updates when topology changes.
5
+ Focused purely on dependency injection - telemetry/tracing is handled at
6
+ the HTTP middleware layer for unified approach across MCP agents and FastAPI apps.
5
7
  """
6
8
 
7
9
  import asyncio
@@ -288,9 +290,27 @@ class DependencyInjector:
288
290
  f"🔧 No injection positions for {func.__name__}, creating minimal wrapper for tracking"
289
291
  )
290
292
 
291
- @functools.wraps(func)
292
- def minimal_wrapper(*args, **kwargs):
293
- return func(*args, **kwargs)
293
+ # Check if we need async wrapper for minimal case
294
+ if inspect.iscoroutinefunction(func):
295
+ @functools.wraps(func)
296
+ async def minimal_wrapper(*args, **kwargs):
297
+ # Use ExecutionTracer for functions without dependencies (v0.4.0 style)
298
+ from ..tracing.execution_tracer import ExecutionTracer
299
+ wrapper_logger.debug(f"🔧 DI: Executing async function {func.__name__} (no dependencies)")
300
+
301
+ # For async functions without dependencies, use the async tracer
302
+ return await ExecutionTracer.trace_function_execution_async(
303
+ func, args, kwargs, [], [], 0, wrapper_logger
304
+ )
305
+ else:
306
+ @functools.wraps(func)
307
+ def minimal_wrapper(*args, **kwargs):
308
+ # Use ExecutionTracer for functions without dependencies (v0.4.0 style)
309
+ from ..tracing.execution_tracer import ExecutionTracer
310
+ wrapper_logger.debug(f"🔧 DI: Executing sync function {func.__name__} (no dependencies)")
311
+
312
+ # Use original function tracer for functions without dependencies
313
+ return ExecutionTracer.trace_original_function(func, args, kwargs, wrapper_logger)
294
314
 
295
315
  # Add minimal metadata for compatibility
296
316
  minimal_wrapper._mesh_injected_deps = {}
@@ -390,20 +410,29 @@ class DependencyInjector:
390
410
  f"🔧 DEPENDENCY_WRAPPER: final_kwargs={final_kwargs}"
391
411
  )
392
412
 
393
- # ===== EXECUTE WITH DEPENDENCY INJECTION =====
394
- # Call the original function with injected dependencies
413
+ # ===== EXECUTE WITH DEPENDENCY INJECTION AND TRACING =====
414
+ # Use ExecutionTracer for comprehensive execution logging (v0.4.0 style)
415
+ from ..tracing.execution_tracer import ExecutionTracer
416
+
395
417
  original_func = func._mesh_original_func
396
418
 
397
- # Check if the function is async and handle accordingly
398
- if inspect.iscoroutinefunction(original_func):
399
- # For async functions, await the result directly
400
- result = await original_func(*args, **final_kwargs)
401
- else:
402
- # For sync functions, call directly
403
- result = original_func(*args, **final_kwargs)
419
+ wrapper_logger.debug(
420
+ f"🔧 DI: Executing async function {original_func.__name__} with {injected_count} injected dependencies"
421
+ )
422
+
423
+ # Use ExecutionTracer's async method for clean tracing
424
+ result = await ExecutionTracer.trace_function_execution_async(
425
+ original_func,
426
+ args,
427
+ final_kwargs,
428
+ dependencies,
429
+ mesh_positions,
430
+ injected_count,
431
+ wrapper_logger,
432
+ )
404
433
 
405
434
  wrapper_logger.debug(
406
- f"🔧 DEPENDENCY_WRAPPER: Original returned: {type(result)}"
435
+ f"🔧 DI: Function {original_func.__name__} returned: {type(result)}"
407
436
  )
408
437
  return result
409
438
 
@@ -454,8 +483,24 @@ class DependencyInjector:
454
483
  final_kwargs[param_name] = dependency
455
484
  injected_count += 1
456
485
 
457
- # Call the original function with injected dependencies
458
- return func._mesh_original_func(*args, **final_kwargs)
486
+ # ===== EXECUTE WITH DEPENDENCY INJECTION AND TRACING =====
487
+ # Use ExecutionTracer for comprehensive execution logging (v0.4.0 style)
488
+ from ..tracing.execution_tracer import ExecutionTracer
489
+
490
+ wrapper_logger.debug(
491
+ f"🔧 DI: Executing sync function {func._mesh_original_func.__name__} with {injected_count} injected dependencies"
492
+ )
493
+
494
+ # Use ExecutionTracer for clean execution tracing
495
+ return ExecutionTracer.trace_function_execution(
496
+ func._mesh_original_func,
497
+ args,
498
+ final_kwargs,
499
+ dependencies,
500
+ mesh_positions,
501
+ injected_count,
502
+ wrapper_logger,
503
+ )
459
504
 
460
505
  # Store dependency state on wrapper
461
506
  dependency_wrapper._mesh_injected_deps = {}
@@ -85,7 +85,7 @@ class FullMCPProxy(MCPClientProxy):
85
85
  try:
86
86
  import httpx
87
87
 
88
- url = f"{self.endpoint}/mcp/"
88
+ url = f"{self.endpoint}/mcp"
89
89
 
90
90
  # Build headers with trace context
91
91
  headers = {
@@ -234,7 +234,7 @@ class FullMCPProxy(MCPClientProxy):
234
234
  }
235
235
 
236
236
  # URL for MCP protocol endpoint
237
- url = f"{self.endpoint.rstrip('/')}/mcp/"
237
+ url = f"{self.endpoint.rstrip('/')}/mcp"
238
238
 
239
239
  # Add session ID to headers for session routing
240
240
  headers = {
@@ -500,7 +500,7 @@ class EnhancedFullMCPProxy(FullMCPProxy):
500
500
  # Inject trace context headers
501
501
  headers = self._inject_trace_headers(headers)
502
502
 
503
- url = f"{self.endpoint}/mcp/"
503
+ url = f"{self.endpoint}/mcp"
504
504
 
505
505
  try:
506
506
  import httpx
@@ -580,7 +580,7 @@ class EnhancedFullMCPProxy(FullMCPProxy):
580
580
  # Inject trace context headers
581
581
  headers = self._inject_trace_headers(headers)
582
582
 
583
- url = f"{self.endpoint}/mcp/"
583
+ url = f"{self.endpoint}/mcp"
584
584
 
585
585
  try:
586
586
  import httpx
@@ -395,14 +395,17 @@ class HttpMcpWrapper:
395
395
  from starlette.responses import Response
396
396
 
397
397
  class MCPSessionRoutingMiddleware(BaseHTTPMiddleware):
398
+ """Clean session routing middleware for MCP requests (v0.4.0 style).
399
+
400
+ Handles session affinity and basic trace context setup only.
401
+ Function execution tracing is handled by ExecutionTracer in DependencyInjector.
402
+ """
398
403
  def __init__(self, app, http_wrapper):
399
404
  super().__init__(app)
400
405
  self.http_wrapper = http_wrapper
401
406
  self.logger = logger
402
407
 
403
408
  async def dispatch(self, request: Request, call_next):
404
- # Only handle MCP requests (FastMCP app already only handles /mcp)
405
-
406
409
  # Extract and set trace context from headers for distributed tracing
407
410
  try:
408
411
  from ..tracing.trace_context_helper import TraceContextHelper
@@ -417,10 +420,8 @@ class HttpMcpWrapper:
417
420
  trace_context, self.logger
418
421
  )
419
422
  except Exception as e:
420
- import logging
421
-
422
- logger = logging.getLogger(__name__)
423
- logger.warning(f"Failed to set trace context: {e}")
423
+ # Never fail request due to tracing issues
424
+ self.logger.warning(f"Failed to set trace context: {e}")
424
425
  pass
425
426
 
426
427
  # Extract session ID from request
@@ -454,7 +455,7 @@ class HttpMcpWrapper:
454
455
 
455
456
  # Add the middleware to FastMCP app
456
457
  self._mcp_app.add_middleware(MCPSessionRoutingMiddleware, http_wrapper=self)
457
- logger.info("✅ Session routing middleware added to FastMCP app")
458
+ logger.info("✅ Clean session routing middleware added to FastMCP app (v0.4.0 style)")
458
459
 
459
460
  async def _extract_session_id(self, request) -> str:
460
461
  """Extract session ID from request headers or body."""
@@ -93,7 +93,7 @@ class MCPClientProxy:
93
93
  "params": {"name": self.function_name, "arguments": kwargs},
94
94
  }
95
95
 
96
- url = f"{self.endpoint}/mcp/" # Use trailing slash to avoid 307 redirect
96
+ url = f"{self.endpoint}/mcp" # Remove trailing slash to avoid 307 redirect
97
97
  data = json.dumps(payload).encode("utf-8")
98
98
 
99
99
  # Build headers with trace context injection