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.
- _mcp_mesh/__init__.py +14 -3
- _mcp_mesh/engine/__init__.py +12 -1
- _mcp_mesh/engine/async_mcp_client.py +2 -2
- _mcp_mesh/engine/decorator_registry.py +98 -8
- _mcp_mesh/engine/dependency_injector.py +249 -71
- _mcp_mesh/engine/full_mcp_proxy.py +4 -4
- _mcp_mesh/engine/http_wrapper.py +9 -20
- _mcp_mesh/engine/mcp_client_proxy.py +1 -1
- _mcp_mesh/engine/unified_mcp_proxy.py +813 -0
- _mcp_mesh/generated/.openapi-generator/FILES +2 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +2 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +1 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +305 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +1 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +10 -1
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +4 -4
- _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +108 -0
- _mcp_mesh/pipeline/__init__.py +2 -2
- _mcp_mesh/pipeline/api_heartbeat/__init__.py +16 -0
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +506 -0
- _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +117 -0
- _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +140 -0
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +247 -0
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +309 -0
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +332 -0
- _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +147 -0
- _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +104 -0
- _mcp_mesh/pipeline/api_startup/__init__.py +20 -0
- _mcp_mesh/pipeline/api_startup/api_pipeline.py +61 -0
- _mcp_mesh/pipeline/api_startup/api_server_setup.py +367 -0
- _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +302 -0
- _mcp_mesh/pipeline/api_startup/route_collection.py +56 -0
- _mcp_mesh/pipeline/api_startup/route_integration.py +318 -0
- _mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/dependency_resolution.py +19 -183
- _mcp_mesh/pipeline/{startup → mcp_startup}/heartbeat_loop.py +1 -1
- _mcp_mesh/pipeline/{startup → mcp_startup}/startup_orchestrator.py +170 -5
- _mcp_mesh/shared/config_resolver.py +0 -3
- _mcp_mesh/shared/logging_config.py +2 -1
- _mcp_mesh/tracing/agent_context_helper.py +1 -1
- _mcp_mesh/tracing/execution_tracer.py +41 -0
- {mcp_mesh-0.4.2.dist-info → mcp_mesh-0.5.1.dist-info}/METADATA +1 -1
- {mcp_mesh-0.4.2.dist-info → mcp_mesh-0.5.1.dist-info}/RECORD +61 -43
- mesh/__init__.py +3 -1
- mesh/decorators.py +143 -1
- mesh/types.py +109 -48
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/__init__.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/fast_heartbeat_check.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_orchestrator.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_pipeline.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/heartbeat_send.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/lifespan_integration.py +0 -0
- /_mcp_mesh/pipeline/{heartbeat → mcp_heartbeat}/registry_connection.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/__init__.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/configuration.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/decorator_collection.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/fastapiserver_setup.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/fastmcpserver_discovery.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/heartbeat_preparation.py +0 -0
- /_mcp_mesh/pipeline/{startup → mcp_startup}/startup_pipeline.py +0 -0
- {mcp_mesh-0.4.2.dist-info → mcp_mesh-0.5.1.dist-info}/WHEEL +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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__ = [
|
_mcp_mesh/engine/__init__.py
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
f"🔧
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
348
|
+
self._function_registry[func_id] = minimal_wrapper
|
|
349
|
+
|
|
350
|
+
return minimal_wrapper
|
|
292
351
|
|
|
293
|
-
|
|
294
|
-
|
|
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:
|
|
360
|
+
f"🔧 DEPENDENCY_WRAPPER: Function {func.__name__} called"
|
|
297
361
|
)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
310
|
-
|
|
374
|
+
# Get function signature
|
|
375
|
+
sig = inspect.signature(func)
|
|
376
|
+
params = list(sig.parameters.keys())
|
|
377
|
+
final_kwargs = kwargs.copy()
|
|
311
378
|
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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:
|
|
390
|
+
f"🔧 DEPENDENCY_WRAPPER: Processing dep {dep_index}: {dep_name} -> {param_name}"
|
|
334
391
|
)
|
|
335
392
|
|
|
336
|
-
if
|
|
337
|
-
|
|
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
|
|
403
|
+
f"🔧 DEPENDENCY_WRAPPER: From wrapper storage: {dependency}"
|
|
340
404
|
)
|
|
341
405
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
)
|
|
374
|
-
|
|
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
|
_mcp_mesh/engine/http_wrapper.py
CHANGED
|
@@ -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
|
-
#
|
|
407
|
-
|
|
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)
|