mcp-mesh 0.4.2__py3-none-any.whl → 0.5.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 (61) hide show
  1. _mcp_mesh/__init__.py +14 -3
  2. _mcp_mesh/engine/__init__.py +12 -1
  3. _mcp_mesh/engine/async_mcp_client.py +2 -2
  4. _mcp_mesh/engine/decorator_registry.py +98 -8
  5. _mcp_mesh/engine/dependency_injector.py +249 -71
  6. _mcp_mesh/engine/full_mcp_proxy.py +4 -4
  7. _mcp_mesh/engine/http_wrapper.py +9 -20
  8. _mcp_mesh/engine/mcp_client_proxy.py +1 -1
  9. _mcp_mesh/engine/unified_mcp_proxy.py +813 -0
  10. _mcp_mesh/generated/.openapi-generator/FILES +2 -0
  11. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +2 -0
  12. _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +1 -0
  13. _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +305 -0
  14. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +1 -0
  15. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +10 -1
  16. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +4 -4
  17. _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +108 -0
  18. _mcp_mesh/pipeline/__init__.py +2 -2
  19. _mcp_mesh/pipeline/api_heartbeat/__init__.py +16 -0
  20. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +506 -0
  21. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +117 -0
  22. _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +140 -0
  23. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +247 -0
  24. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +309 -0
  25. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +332 -0
  26. _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +147 -0
  27. _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +104 -0
  28. _mcp_mesh/pipeline/api_startup/__init__.py +20 -0
  29. _mcp_mesh/pipeline/api_startup/api_pipeline.py +61 -0
  30. _mcp_mesh/pipeline/api_startup/api_server_setup.py +367 -0
  31. _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +302 -0
  32. _mcp_mesh/pipeline/api_startup/route_collection.py +56 -0
  33. _mcp_mesh/pipeline/api_startup/route_integration.py +318 -0
  34. _mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/dependency_resolution.py +19 -183
  35. _mcp_mesh/pipeline/{startup → mcp_startup}/heartbeat_loop.py +1 -1
  36. _mcp_mesh/pipeline/{startup → mcp_startup}/startup_orchestrator.py +170 -5
  37. _mcp_mesh/shared/config_resolver.py +0 -3
  38. _mcp_mesh/shared/logging_config.py +2 -1
  39. _mcp_mesh/tracing/agent_context_helper.py +1 -1
  40. _mcp_mesh/tracing/execution_tracer.py +41 -0
  41. {mcp_mesh-0.4.2.dist-info → mcp_mesh-0.5.1.dist-info}/METADATA +1 -1
  42. {mcp_mesh-0.4.2.dist-info → mcp_mesh-0.5.1.dist-info}/RECORD +61 -43
  43. mesh/__init__.py +3 -1
  44. mesh/decorators.py +143 -1
  45. mesh/types.py +109 -48
  46. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/__init__.py +0 -0
  47. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/fast_heartbeat_check.py +0 -0
  48. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_orchestrator.py +0 -0
  49. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_pipeline.py +0 -0
  50. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_send.py +0 -0
  51. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/lifespan_integration.py +0 -0
  52. /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/registry_connection.py +0 -0
  53. /_mcp_mesh/pipeline/{startup → mcp_startup}/__init__.py +0 -0
  54. /_mcp_mesh/pipeline/{startup → mcp_startup}/configuration.py +0 -0
  55. /_mcp_mesh/pipeline/{startup → mcp_startup}/decorator_collection.py +0 -0
  56. /_mcp_mesh/pipeline/{startup → mcp_startup}/fastapiserver_setup.py +0 -0
  57. /_mcp_mesh/pipeline/{startup → mcp_startup}/fastmcpserver_discovery.py +0 -0
  58. /_mcp_mesh/pipeline/{startup → mcp_startup}/heartbeat_preparation.py +0 -0
  59. /_mcp_mesh/pipeline/{startup → mcp_startup}/startup_pipeline.py +0 -0
  60. {mcp_mesh-0.4.2.dist-info → mcp_mesh-0.5.1.dist-info}/WHEEL +0 -0
  61. {mcp_mesh-0.4.2.dist-info → mcp_mesh-0.5.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.4.2"
34
+ __version__ = "0.5.1"
35
35
 
36
36
  # Store reference to runtime processor if initialized
37
37
  _runtime_processor = None
@@ -48,7 +48,7 @@ def initialize_runtime():
48
48
  # Legacy processor system has been replaced by pipeline architecture
49
49
 
50
50
  # Use pipeline-based runtime
51
- from .pipeline.startup import start_runtime
51
+ from .pipeline.mcp_startup import start_runtime
52
52
 
53
53
  start_runtime()
54
54
 
@@ -60,7 +60,18 @@ def initialize_runtime():
60
60
 
61
61
  # Auto-initialize runtime if enabled
62
62
  if os.getenv("MCP_MESH_ENABLED", "true").lower() == "true":
63
- initialize_runtime()
63
+ # Use debounced initialization instead of immediate MCP startup
64
+ # This allows the system to determine MCP vs API pipeline based on decorators
65
+ try:
66
+ from .pipeline.mcp_startup import start_runtime
67
+
68
+ # Start the debounced runtime (sets up coordinator, no immediate pipeline execution)
69
+ start_runtime()
70
+
71
+ sys.stderr.write("MCP Mesh debounced runtime initialized\n")
72
+ except Exception as e:
73
+ # Log but don't fail - allows graceful degradation
74
+ sys.stderr.write(f"MCP Mesh runtime initialization failed: {e}\n")
64
75
 
65
76
 
66
77
  __all__ = [
@@ -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
  """
@@ -126,6 +126,9 @@ class DependencyInjector:
126
126
  for func_id in self._dependency_mapping[name]:
127
127
  if func_id in self._function_registry:
128
128
  func = self._function_registry[func_id]
129
+ logger.debug(
130
+ f"🔄 UPDATING dependency '{name}' for {func_id} -> {func} at {hex(id(func))}"
131
+ )
129
132
  if hasattr(func, "_mesh_update_dependency"):
130
133
  func._mesh_update_dependency(name, instance)
131
134
 
@@ -279,99 +282,271 @@ class DependencyInjector:
279
282
  # Capture logger in local scope to avoid NameError
280
283
  wrapper_logger = logger
281
284
 
282
- @functools.wraps(func)
283
- def dependency_wrapper(*args, **kwargs):
284
- wrapper_logger.debug(
285
- f"🔧 DEPENDENCY_WRAPPER: Function {func.__name__} called"
285
+ # If no mesh positions to inject, create minimal wrapper for tracking
286
+ if not mesh_positions:
287
+ logger.debug(
288
+ f"🔧 No injection positions for {func.__name__}, creating minimal wrapper for tracking"
286
289
  )
287
- wrapper_logger.debug(f"🔧 DEPENDENCY_WRAPPER: args={args}, kwargs={kwargs}")
288
- wrapper_logger.debug(
289
- f"🔧 DEPENDENCY_WRAPPER: mesh_positions={mesh_positions}"
290
+
291
+ # Check if we need async wrapper for minimal case
292
+ if inspect.iscoroutinefunction(func):
293
+ @functools.wraps(func)
294
+ async def minimal_wrapper(*args, **kwargs):
295
+ # Execute with telemetry tracing even for async functions without dependencies
296
+ try:
297
+ from ..tracing.execution_tracer import ExecutionTracer
298
+
299
+ # Use ExecutionTracer for minimal async wrapper (no dependencies)
300
+ return await ExecutionTracer.trace_function_execution_async(
301
+ func, args, kwargs, [], [], 0, wrapper_logger
302
+ )
303
+ except ImportError:
304
+ # Fallback if tracing is unavailable - never fail user function
305
+ wrapper_logger.debug("🔇 Tracing unavailable, executing without telemetry")
306
+ return await func(*args, **kwargs)
307
+ except Exception as e:
308
+ # Never fail user function due to tracing errors
309
+ wrapper_logger.warning(f"⚠️ Telemetry failed, executing without: {e}")
310
+ return await func(*args, **kwargs)
311
+ else:
312
+ @functools.wraps(func)
313
+ def minimal_wrapper(*args, **kwargs):
314
+ # Execute with telemetry tracing even for functions without dependencies
315
+ try:
316
+ from ..tracing.execution_tracer import ExecutionTracer
317
+
318
+ # Use ExecutionTracer for minimal wrapper (no dependencies)
319
+ return ExecutionTracer.trace_original_function(
320
+ func, args, kwargs, wrapper_logger
321
+ )
322
+ except ImportError:
323
+ # Fallback if tracing is unavailable - never fail user function
324
+ wrapper_logger.debug("🔇 Tracing unavailable, executing without telemetry")
325
+ return func(*args, **kwargs)
326
+ except Exception as e:
327
+ # Never fail user function due to tracing errors
328
+ wrapper_logger.warning(f"⚠️ Telemetry failed, executing without: {e}")
329
+ return func(*args, **kwargs)
330
+
331
+ # Add minimal metadata for compatibility
332
+ minimal_wrapper._mesh_injected_deps = {}
333
+ minimal_wrapper._mesh_dependencies = dependencies
334
+ minimal_wrapper._mesh_positions = mesh_positions
335
+ minimal_wrapper._mesh_parameter_types = get_agent_parameter_types(func)
336
+ minimal_wrapper._mesh_original_func = func
337
+
338
+ def update_dependency(name: str, instance: Any | None) -> None:
339
+ """No-op update for functions without injection positions."""
340
+ pass
341
+
342
+ minimal_wrapper._mesh_update_dependency = update_dependency
343
+
344
+ # Register this wrapper for dependency updates (even though it won't use them)
345
+ logger.debug(
346
+ f"🔧 REGISTERING minimal wrapper: {func_id} -> {minimal_wrapper} at {hex(id(minimal_wrapper))}"
290
347
  )
291
- wrapper_logger.debug(f"🔧 DEPENDENCY_WRAPPER: dependencies={dependencies}")
348
+ self._function_registry[func_id] = minimal_wrapper
349
+
350
+ return minimal_wrapper
292
351
 
293
- # If no mesh positions to inject into, still do execution logging but call original function
294
- if not mesh_positions:
352
+ # Determine if we need async wrapper
353
+ need_async_wrapper = inspect.iscoroutinefunction(func)
354
+
355
+ if need_async_wrapper:
356
+
357
+ @functools.wraps(func)
358
+ async def dependency_wrapper(*args, **kwargs):
295
359
  wrapper_logger.debug(
296
- "🔧 DEPENDENCY_WRAPPER: No mesh positions, calling original with execution logging"
360
+ f"🔧 DEPENDENCY_WRAPPER: Function {func.__name__} called"
297
361
  )
298
- from ..tracing.execution_tracer import ExecutionTracer
299
-
300
- return ExecutionTracer.trace_original_function(
301
- func._mesh_original_func, args, kwargs, wrapper_logger
362
+ wrapper_logger.debug(
363
+ f"🔧 DEPENDENCY_WRAPPER: args={args}, kwargs={kwargs}"
364
+ )
365
+ wrapper_logger.debug(
366
+ f"🔧 DEPENDENCY_WRAPPER: mesh_positions={mesh_positions}"
367
+ )
368
+ wrapper_logger.debug(
369
+ f"🔧 DEPENDENCY_WRAPPER: dependencies={dependencies}"
302
370
  )
303
371
 
304
- # Get function signature
305
- sig = inspect.signature(func)
306
- params = list(sig.parameters.keys())
307
- final_kwargs = kwargs.copy()
372
+ # We know mesh_positions is not empty since we checked above
308
373
 
309
- wrapper_logger.debug(f"🔧 DEPENDENCY_WRAPPER: params={params}")
310
- wrapper_logger.debug(f"🔧 DEPENDENCY_WRAPPER: original kwargs={kwargs}")
374
+ # Get function signature
375
+ sig = inspect.signature(func)
376
+ params = list(sig.parameters.keys())
377
+ final_kwargs = kwargs.copy()
311
378
 
312
- # Inject dependencies as kwargs
313
- injected_count = 0
314
- for dep_index, param_position in enumerate(mesh_positions):
315
- if dep_index < len(dependencies):
316
- dep_name = dependencies[dep_index]
317
- param_name = params[param_position]
379
+ wrapper_logger.debug(f"🔧 DEPENDENCY_WRAPPER: params={params}")
380
+ wrapper_logger.debug(f"🔧 DEPENDENCY_WRAPPER: original kwargs={kwargs}")
318
381
 
319
- wrapper_logger.debug(
320
- f"🔧 DEPENDENCY_WRAPPER: Processing dep {dep_index}: {dep_name} -> {param_name}"
321
- )
382
+ # Inject dependencies as kwargs
383
+ injected_count = 0
384
+ for dep_index, param_position in enumerate(mesh_positions):
385
+ if dep_index < len(dependencies):
386
+ dep_name = dependencies[dep_index]
387
+ param_name = params[param_position]
322
388
 
323
- # Only inject if the parameter wasn't explicitly provided
324
- if (
325
- param_name not in final_kwargs
326
- or final_kwargs.get(param_name) is None
327
- ):
328
- # Get the dependency from wrapper's storage
329
- dependency = dependency_wrapper._mesh_injected_deps.get(
330
- dep_name
331
- )
332
389
  wrapper_logger.debug(
333
- f"🔧 DEPENDENCY_WRAPPER: From wrapper storage: {dependency}"
390
+ f"🔧 DEPENDENCY_WRAPPER: Processing dep {dep_index}: {dep_name} -> {param_name}"
334
391
  )
335
392
 
336
- if dependency is None:
337
- dependency = self.get_dependency(dep_name)
393
+ # Only inject if the parameter wasn't explicitly provided
394
+ if (
395
+ param_name not in final_kwargs
396
+ or final_kwargs.get(param_name) is None
397
+ ):
398
+ # Get the dependency from wrapper's storage
399
+ dependency = dependency_wrapper._mesh_injected_deps.get(
400
+ dep_name
401
+ )
338
402
  wrapper_logger.debug(
339
- f"🔧 DEPENDENCY_WRAPPER: From global storage: {dependency}"
403
+ f"🔧 DEPENDENCY_WRAPPER: From wrapper storage: {dependency}"
340
404
  )
341
405
 
342
- final_kwargs[param_name] = dependency
343
- injected_count += 1
344
- wrapper_logger.debug(
345
- f"🔧 DEPENDENCY_WRAPPER: Injected {dep_name} as {param_name}"
406
+ if dependency is None:
407
+ dependency = self.get_dependency(dep_name)
408
+ wrapper_logger.debug(
409
+ f"🔧 DEPENDENCY_WRAPPER: From global storage: {dependency}"
410
+ )
411
+
412
+ final_kwargs[param_name] = dependency
413
+ injected_count += 1
414
+ wrapper_logger.debug(
415
+ f"🔧 DEPENDENCY_WRAPPER: Injected {dep_name} as {param_name}"
416
+ )
417
+ else:
418
+ wrapper_logger.debug(
419
+ f"🔧 DEPENDENCY_WRAPPER: Skipping {param_name} - already provided"
420
+ )
421
+
422
+ wrapper_logger.debug(
423
+ f"🔧 DEPENDENCY_WRAPPER: Injected {injected_count} dependencies"
424
+ )
425
+ wrapper_logger.debug(
426
+ f"🔧 DEPENDENCY_WRAPPER: final_kwargs={final_kwargs}"
427
+ )
428
+
429
+ # ===== EXECUTE WITH TELEMETRY AND DEPENDENCY INJECTION =====
430
+ # Call the original function with telemetry tracing and injected dependencies
431
+ original_func = func._mesh_original_func
432
+
433
+ # Execute with telemetry tracing
434
+ try:
435
+ from ..tracing.execution_tracer import ExecutionTracer
436
+
437
+ # Use ExecutionTracer for comprehensive telemetry
438
+ if inspect.iscoroutinefunction(original_func):
439
+ # For async functions, await the result directly
440
+ result = await ExecutionTracer.trace_function_execution_async(
441
+ original_func,
442
+ args,
443
+ final_kwargs,
444
+ dependencies,
445
+ mesh_positions,
446
+ injected_count,
447
+ wrapper_logger,
346
448
  )
347
449
  else:
348
- wrapper_logger.debug(
349
- f"🔧 DEPENDENCY_WRAPPER: Skipping {param_name} - already provided"
450
+ # For sync functions, call directly
451
+ result = ExecutionTracer.trace_function_execution(
452
+ original_func,
453
+ args,
454
+ final_kwargs,
455
+ dependencies,
456
+ mesh_positions,
457
+ injected_count,
458
+ wrapper_logger,
350
459
  )
460
+ except ImportError:
461
+ # Fallback if tracing is unavailable - never fail user function
462
+ wrapper_logger.debug("🔇 Tracing unavailable, executing without telemetry")
463
+ if inspect.iscoroutinefunction(original_func):
464
+ result = await original_func(*args, **final_kwargs)
465
+ else:
466
+ result = original_func(*args, **final_kwargs)
467
+ except Exception as e:
468
+ # Never fail user function due to tracing errors
469
+ wrapper_logger.warning(f"⚠️ Telemetry failed, executing without: {e}")
470
+ if inspect.iscoroutinefunction(original_func):
471
+ result = await original_func(*args, **final_kwargs)
472
+ else:
473
+ result = original_func(*args, **final_kwargs)
351
474
 
352
- wrapper_logger.debug(
353
- f"🔧 DEPENDENCY_WRAPPER: Injected {injected_count} dependencies"
354
- )
355
- wrapper_logger.debug(f"🔧 DEPENDENCY_WRAPPER: final_kwargs={final_kwargs}")
356
-
357
- # ===== EXECUTE WITH DEPENDENCY INJECTION AND TRACING =====
358
- from ..tracing.execution_tracer import ExecutionTracer
359
-
360
- # Use helper class for clean execution tracing
361
- result = ExecutionTracer.trace_function_execution(
362
- func._mesh_original_func,
363
- args,
364
- final_kwargs,
365
- dependencies,
366
- mesh_positions,
367
- injected_count,
368
- wrapper_logger,
369
- )
475
+ wrapper_logger.debug(
476
+ f"🔧 DEPENDENCY_WRAPPER: Original returned: {type(result)}"
477
+ )
478
+ return result
370
479
 
371
- wrapper_logger.debug(
372
- f"🔧 DEPENDENCY_WRAPPER: Original returned: {type(result)}"
373
- )
374
- return result
480
+ else:
481
+ # Create sync wrapper for sync functions without dependencies
482
+ @functools.wraps(func)
483
+ def dependency_wrapper(*args, **kwargs):
484
+ wrapper_logger.debug(
485
+ f"🔧 DEPENDENCY_WRAPPER: Function {func.__name__} called"
486
+ )
487
+ wrapper_logger.debug(
488
+ f"🔧 DEPENDENCY_WRAPPER: args={args}, kwargs={kwargs}"
489
+ )
490
+ wrapper_logger.debug(
491
+ f"🔧 DEPENDENCY_WRAPPER: mesh_positions={mesh_positions}"
492
+ )
493
+ wrapper_logger.debug(
494
+ f"🔧 DEPENDENCY_WRAPPER: dependencies={dependencies}"
495
+ )
496
+
497
+ # We know mesh_positions is not empty since we checked above
498
+
499
+ # Handle dependency injection for sync functions
500
+ sig = inspect.signature(func)
501
+ params = list(sig.parameters.keys())
502
+ final_kwargs = kwargs.copy()
503
+
504
+ # Inject dependencies as kwargs
505
+ injected_count = 0
506
+ for dep_index, param_position in enumerate(mesh_positions):
507
+ if dep_index < len(dependencies):
508
+ dep_name = dependencies[dep_index]
509
+ param_name = params[param_position]
510
+
511
+ # Only inject if the parameter wasn't explicitly provided
512
+ if (
513
+ param_name not in final_kwargs
514
+ or final_kwargs.get(param_name) is None
515
+ ):
516
+ # Get the dependency from wrapper's storage
517
+ dependency = dependency_wrapper._mesh_injected_deps.get(
518
+ dep_name
519
+ )
520
+
521
+ if dependency is None:
522
+ dependency = self.get_dependency(dep_name)
523
+
524
+ final_kwargs[param_name] = dependency
525
+ injected_count += 1
526
+
527
+ # ===== EXECUTE WITH TELEMETRY AND DEPENDENCY INJECTION =====
528
+ # Call the original function with telemetry tracing and injected dependencies
529
+ try:
530
+ from ..tracing.execution_tracer import ExecutionTracer
531
+
532
+ # Use ExecutionTracer for comprehensive telemetry
533
+ return ExecutionTracer.trace_function_execution(
534
+ func._mesh_original_func,
535
+ args,
536
+ final_kwargs,
537
+ dependencies,
538
+ mesh_positions,
539
+ injected_count,
540
+ wrapper_logger,
541
+ )
542
+ except ImportError:
543
+ # Fallback if tracing is unavailable - never fail user function
544
+ wrapper_logger.debug("🔇 Tracing unavailable, executing without telemetry")
545
+ return func._mesh_original_func(*args, **final_kwargs)
546
+ except Exception as e:
547
+ # Never fail user function due to tracing errors
548
+ wrapper_logger.warning(f"⚠️ Telemetry failed, executing without: {e}")
549
+ return func._mesh_original_func(*args, **final_kwargs)
375
550
 
376
551
  # Store dependency state on wrapper
377
552
  dependency_wrapper._mesh_injected_deps = {}
@@ -399,6 +574,9 @@ class DependencyInjector:
399
574
  dependency_wrapper._mesh_original_func = func
400
575
 
401
576
  # Register this wrapper for dependency updates
577
+ logger.debug(
578
+ f"🔧 REGISTERING in function_registry: {func_id} -> {dependency_wrapper} at {hex(id(dependency_wrapper))}"
579
+ )
402
580
  self._function_registry[func_id] = dependency_wrapper
403
581
 
404
582
  # Return the wrapper (which FastMCP will register)
@@ -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,6 +395,12 @@ class HttpMcpWrapper:
395
395
  from starlette.responses import Response
396
396
 
397
397
  class MCPSessionRoutingMiddleware(BaseHTTPMiddleware):
398
+ """Session routing middleware for MCP requests.
399
+
400
+ Handles session affinity by routing requests to appropriate pods
401
+ based on session ID. Telemetry/tracing is now handled in the
402
+ DI function wrapper for unified coverage of both MCP and API calls.
403
+ """
398
404
  def __init__(self, app, http_wrapper):
399
405
  super().__init__(app)
400
406
  self.http_wrapper = http_wrapper
@@ -402,26 +408,9 @@ class HttpMcpWrapper:
402
408
 
403
409
  async def dispatch(self, request: Request, call_next):
404
410
  # Only handle MCP requests (FastMCP app already only handles /mcp)
405
-
406
- # Extract and set trace context from headers for distributed tracing
407
- try:
408
- from ..tracing.trace_context_helper import TraceContextHelper
409
-
410
- # Use helper class for trace context extraction and setup
411
- trace_context = (
412
- await TraceContextHelper.extract_trace_context_from_request(
413
- request
414
- )
415
- )
416
- TraceContextHelper.setup_request_trace_context(
417
- trace_context, self.logger
418
- )
419
- except Exception as e:
420
- import logging
421
-
422
- logger = logging.getLogger(__name__)
423
- logger.warning(f"Failed to set trace context: {e}")
424
- pass
411
+
412
+ # Note: Telemetry/tracing now handled in DI function wrapper for unified approach
413
+ # This middleware focuses purely on session routing
425
414
 
426
415
  # Extract session ID from request
427
416
  session_id = await self.http_wrapper._extract_session_id(request)