mcp-mesh 0.6.4__py3-none-any.whl → 0.7.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 +1 -1
- _mcp_mesh/engine/decorator_registry.py +50 -11
- _mcp_mesh/engine/http_wrapper.py +10 -2
- _mcp_mesh/engine/mesh_llm_agent.py +98 -6
- _mcp_mesh/engine/unified_mcp_proxy.py +10 -2
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +82 -100
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +150 -96
- _mcp_mesh/pipeline/api_startup/route_integration.py +91 -92
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +7 -0
- _mcp_mesh/tracing/execution_tracer.py +41 -13
- {mcp_mesh-0.6.4.dist-info → mcp_mesh-0.7.1.dist-info}/METADATA +1 -1
- {mcp_mesh-0.6.4.dist-info → mcp_mesh-0.7.1.dist-info}/RECORD +15 -15
- mesh/decorators.py +43 -0
- {mcp_mesh-0.6.4.dist-info → mcp_mesh-0.7.1.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.6.4.dist-info → mcp_mesh-0.7.1.dist-info}/licenses/LICENSE +0 -0
_mcp_mesh/__init__.py
CHANGED
|
@@ -38,7 +38,7 @@ class LLMAgentMetadata:
|
|
|
38
38
|
|
|
39
39
|
function: Callable
|
|
40
40
|
config: dict[str, Any] # LLM configuration (provider, model, filter, etc.)
|
|
41
|
-
output_type:
|
|
41
|
+
output_type: type | None # Pydantic model type from return annotation
|
|
42
42
|
param_name: str # Name of MeshLlmAgent parameter
|
|
43
43
|
function_id: str # Unique function ID for registry
|
|
44
44
|
registered_at: datetime
|
|
@@ -70,16 +70,55 @@ class DecoratorRegistry:
|
|
|
70
70
|
_custom_decorators: dict[str, dict[str, DecoratedFunction]] = {}
|
|
71
71
|
|
|
72
72
|
# Immediate uvicorn server storage (for preventing shutdown state)
|
|
73
|
-
_immediate_uvicorn_server:
|
|
73
|
+
_immediate_uvicorn_server: dict[str, Any] | None = None
|
|
74
74
|
|
|
75
75
|
# FastMCP lifespan storage (for proper integration with FastAPI)
|
|
76
|
-
_fastmcp_lifespan:
|
|
76
|
+
_fastmcp_lifespan: Any | None = None
|
|
77
77
|
|
|
78
78
|
# FastMCP HTTP app storage (the same app instance whose lifespan was extracted)
|
|
79
|
-
_fastmcp_http_app:
|
|
79
|
+
_fastmcp_http_app: Any | None = None
|
|
80
80
|
|
|
81
81
|
# FastMCP server info storage (for schema extraction during heartbeat)
|
|
82
|
-
_fastmcp_server_info:
|
|
82
|
+
_fastmcp_server_info: dict[str, Any] | None = None
|
|
83
|
+
|
|
84
|
+
# Route-to-wrapper mapping for @mesh.route dependency injection
|
|
85
|
+
# Key: "METHOD:path" (e.g., "GET:/api/v1/benchmark-services")
|
|
86
|
+
# Value: {"wrapper": Callable, "dependencies": list[str]}
|
|
87
|
+
_route_wrapper_registry: dict[str, dict[str, Any]] = {}
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def register_route_wrapper(
|
|
91
|
+
cls, method: str, path: str, wrapper: Callable, dependencies: list[str]
|
|
92
|
+
) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Register a route's wrapper function for dependency injection.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
method: HTTP method (e.g., "GET", "POST")
|
|
98
|
+
path: Route path (e.g., "/api/v1/benchmark-services")
|
|
99
|
+
wrapper: The injection wrapper function
|
|
100
|
+
dependencies: List of dependency capability names
|
|
101
|
+
"""
|
|
102
|
+
route_id = f"{method}:{path}"
|
|
103
|
+
cls._route_wrapper_registry[route_id] = {
|
|
104
|
+
"wrapper": wrapper,
|
|
105
|
+
"dependencies": dependencies,
|
|
106
|
+
"method": method,
|
|
107
|
+
"path": path,
|
|
108
|
+
}
|
|
109
|
+
logger.debug(
|
|
110
|
+
f"📝 Registered route wrapper: {route_id} with {len(dependencies)} dependencies"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def get_route_wrapper(cls, route_id: str) -> dict[str, Any] | None:
|
|
115
|
+
"""Get route wrapper info by route ID (METHOD:path)."""
|
|
116
|
+
return cls._route_wrapper_registry.get(route_id)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def get_all_route_wrappers(cls) -> dict[str, dict[str, Any]]:
|
|
120
|
+
"""Get all registered route wrappers."""
|
|
121
|
+
return cls._route_wrapper_registry.copy()
|
|
83
122
|
|
|
84
123
|
@classmethod
|
|
85
124
|
def register_mesh_agent(cls, func: Callable, metadata: dict[str, Any]) -> None:
|
|
@@ -154,7 +193,7 @@ class DecoratorRegistry:
|
|
|
154
193
|
cls,
|
|
155
194
|
func: Callable,
|
|
156
195
|
config: dict[str, Any],
|
|
157
|
-
output_type:
|
|
196
|
+
output_type: type | None,
|
|
158
197
|
param_name: str,
|
|
159
198
|
function_id: str,
|
|
160
199
|
) -> None:
|
|
@@ -355,7 +394,7 @@ class DecoratorRegistry:
|
|
|
355
394
|
return stats
|
|
356
395
|
|
|
357
396
|
# Cache for resolved agent configuration to avoid repeated work
|
|
358
|
-
_cached_agent_config:
|
|
397
|
+
_cached_agent_config: dict[str, Any] | None = None
|
|
359
398
|
|
|
360
399
|
@classmethod
|
|
361
400
|
def update_agent_config(cls, updates: dict[str, Any]) -> None:
|
|
@@ -610,7 +649,7 @@ class DecoratorRegistry:
|
|
|
610
649
|
)
|
|
611
650
|
|
|
612
651
|
@classmethod
|
|
613
|
-
def get_immediate_uvicorn_server(cls) ->
|
|
652
|
+
def get_immediate_uvicorn_server(cls) -> dict[str, Any] | None:
|
|
614
653
|
"""
|
|
615
654
|
Get stored immediate uvicorn server reference.
|
|
616
655
|
|
|
@@ -659,7 +698,7 @@ class DecoratorRegistry:
|
|
|
659
698
|
logger.debug("🔄 REGISTRY: Stored FastMCP lifespan for FastAPI integration")
|
|
660
699
|
|
|
661
700
|
@classmethod
|
|
662
|
-
def get_fastmcp_lifespan(cls) ->
|
|
701
|
+
def get_fastmcp_lifespan(cls) -> Any | None:
|
|
663
702
|
"""
|
|
664
703
|
Get stored FastMCP lifespan.
|
|
665
704
|
|
|
@@ -686,7 +725,7 @@ class DecoratorRegistry:
|
|
|
686
725
|
logger.debug("🔄 REGISTRY: Stored FastMCP HTTP app for mounting")
|
|
687
726
|
|
|
688
727
|
@classmethod
|
|
689
|
-
def get_fastmcp_http_app(cls) ->
|
|
728
|
+
def get_fastmcp_http_app(cls) -> Any | None:
|
|
690
729
|
"""
|
|
691
730
|
Get stored FastMCP HTTP app.
|
|
692
731
|
|
|
@@ -715,7 +754,7 @@ class DecoratorRegistry:
|
|
|
715
754
|
)
|
|
716
755
|
|
|
717
756
|
@classmethod
|
|
718
|
-
def get_fastmcp_server_info(cls) ->
|
|
757
|
+
def get_fastmcp_server_info(cls) -> dict[str, Any] | None:
|
|
719
758
|
"""
|
|
720
759
|
Get stored FastMCP server info.
|
|
721
760
|
|
_mcp_mesh/engine/http_wrapper.py
CHANGED
|
@@ -132,7 +132,7 @@ class HttpMcpWrapper:
|
|
|
132
132
|
|
|
133
133
|
# Phase 3: Metadata caching
|
|
134
134
|
self._metadata_cache: dict[str, Any] = {}
|
|
135
|
-
self._cache_timestamp:
|
|
135
|
+
self._cache_timestamp: datetime | None = None
|
|
136
136
|
self._cache_ttl: timedelta = timedelta(minutes=5) # Cache for 5 minutes
|
|
137
137
|
|
|
138
138
|
# Phase 5: Session storage and pod info
|
|
@@ -254,7 +254,7 @@ class HttpMcpWrapper:
|
|
|
254
254
|
self._cache_timestamp = datetime.now()
|
|
255
255
|
logger.debug(f"📋 Metadata cache updated with {len(metadata)} entries")
|
|
256
256
|
|
|
257
|
-
def get_cached_metadata(self) ->
|
|
257
|
+
def get_cached_metadata(self) -> dict[str, Any] | None:
|
|
258
258
|
"""Get cached metadata if available and valid."""
|
|
259
259
|
if self._is_cache_valid():
|
|
260
260
|
logger.debug("✅ Returning cached metadata")
|
|
@@ -391,6 +391,14 @@ class HttpMcpWrapper:
|
|
|
391
391
|
try:
|
|
392
392
|
from ..tracing.trace_context_helper import TraceContextHelper
|
|
393
393
|
|
|
394
|
+
# DEBUG: Log incoming headers for trace propagation debugging
|
|
395
|
+
trace_id_header = request.headers.get("X-Trace-ID")
|
|
396
|
+
parent_span_header = request.headers.get("X-Parent-Span")
|
|
397
|
+
self.logger.info(
|
|
398
|
+
f"🔍 INCOMING_HEADERS: X-Trace-ID={trace_id_header}, "
|
|
399
|
+
f"X-Parent-Span={parent_span_header}, path={request.url.path}"
|
|
400
|
+
)
|
|
401
|
+
|
|
394
402
|
# Use helper class for trace context extraction and setup
|
|
395
403
|
trace_context = (
|
|
396
404
|
await TraceContextHelper.extract_trace_context_from_request(
|
|
@@ -8,7 +8,7 @@ import asyncio
|
|
|
8
8
|
import json
|
|
9
9
|
import logging
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Dict, List, Optional, Union
|
|
11
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
12
12
|
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
|
|
@@ -41,6 +41,9 @@ except ImportError:
|
|
|
41
41
|
|
|
42
42
|
logger = logging.getLogger(__name__)
|
|
43
43
|
|
|
44
|
+
# Sentinel value to distinguish "context not provided" from "explicitly None/empty"
|
|
45
|
+
_CONTEXT_NOT_PROVIDED = object()
|
|
46
|
+
|
|
44
47
|
|
|
45
48
|
class MeshLlmAgent:
|
|
46
49
|
"""
|
|
@@ -241,7 +244,56 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
241
244
|
f"Expected MeshContextModel, dict, or None."
|
|
242
245
|
)
|
|
243
246
|
|
|
244
|
-
def
|
|
247
|
+
def _resolve_context(
|
|
248
|
+
self,
|
|
249
|
+
runtime_context: Union[dict, None, object],
|
|
250
|
+
context_mode: Literal["replace", "append", "prepend"],
|
|
251
|
+
) -> dict:
|
|
252
|
+
"""
|
|
253
|
+
Resolve effective context for template rendering.
|
|
254
|
+
|
|
255
|
+
Merges auto-populated context (from decorator's context_param) with
|
|
256
|
+
runtime context passed to __call__(), based on the context_mode.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
runtime_context: Context passed at call time, or _CONTEXT_NOT_PROVIDED
|
|
260
|
+
context_mode: How to merge contexts - "replace", "append", or "prepend"
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Resolved context dictionary for template rendering
|
|
264
|
+
|
|
265
|
+
Behavior:
|
|
266
|
+
- If runtime_context is _CONTEXT_NOT_PROVIDED: use auto-populated context
|
|
267
|
+
- If context_mode is "replace": use runtime_context entirely
|
|
268
|
+
- If context_mode is "append": auto_context | runtime_context (runtime wins)
|
|
269
|
+
- If context_mode is "prepend": runtime_context | auto_context (auto wins)
|
|
270
|
+
|
|
271
|
+
Note:
|
|
272
|
+
Empty dict {} with "replace" mode explicitly clears context.
|
|
273
|
+
Empty dict {} with "append"/"prepend" is a no-op (keeps auto context).
|
|
274
|
+
"""
|
|
275
|
+
# Get auto-populated context from decorator
|
|
276
|
+
auto_context = self._prepare_context(self._context_value)
|
|
277
|
+
|
|
278
|
+
# If no runtime context provided, use auto-populated context unchanged
|
|
279
|
+
if runtime_context is _CONTEXT_NOT_PROVIDED:
|
|
280
|
+
return auto_context
|
|
281
|
+
|
|
282
|
+
# Prepare runtime context (handles MeshContextModel, dict, None)
|
|
283
|
+
runtime_dict = self._prepare_context(runtime_context)
|
|
284
|
+
|
|
285
|
+
# Apply context_mode
|
|
286
|
+
if context_mode == "replace":
|
|
287
|
+
# Replace entirely with runtime context (even if empty)
|
|
288
|
+
return runtime_dict
|
|
289
|
+
elif context_mode == "prepend":
|
|
290
|
+
# Runtime first, auto overwrites (auto wins on conflicts)
|
|
291
|
+
return {**runtime_dict, **auto_context}
|
|
292
|
+
else: # "append" (default)
|
|
293
|
+
# Auto first, runtime overwrites (runtime wins on conflicts)
|
|
294
|
+
return {**auto_context, **runtime_dict}
|
|
295
|
+
|
|
296
|
+
def _render_system_prompt(self, effective_context: Optional[dict] = None) -> str:
|
|
245
297
|
"""
|
|
246
298
|
Render system prompt from template or return literal.
|
|
247
299
|
|
|
@@ -249,6 +301,10 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
249
301
|
If system_prompt was set via set_system_prompt(), uses that override.
|
|
250
302
|
Otherwise, uses config.system_prompt as literal.
|
|
251
303
|
|
|
304
|
+
Args:
|
|
305
|
+
effective_context: Optional pre-resolved context dict for template rendering.
|
|
306
|
+
If None, uses auto-populated _context_value.
|
|
307
|
+
|
|
252
308
|
Returns:
|
|
253
309
|
Rendered system prompt string
|
|
254
310
|
|
|
@@ -261,7 +317,12 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
261
317
|
|
|
262
318
|
# If template provided, render it
|
|
263
319
|
if self._template is not None:
|
|
264
|
-
|
|
320
|
+
# Use provided effective_context or fall back to auto-populated context
|
|
321
|
+
context = (
|
|
322
|
+
effective_context
|
|
323
|
+
if effective_context is not None
|
|
324
|
+
else self._prepare_context(self._context_value)
|
|
325
|
+
)
|
|
265
326
|
try:
|
|
266
327
|
rendered = self._template.render(**context)
|
|
267
328
|
logger.debug(
|
|
@@ -412,7 +473,12 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
412
473
|
raise RuntimeError(f"Mesh LLM provider invocation failed: {e}") from e
|
|
413
474
|
|
|
414
475
|
async def __call__(
|
|
415
|
-
self,
|
|
476
|
+
self,
|
|
477
|
+
message: Union[str, list[dict[str, Any]]],
|
|
478
|
+
*,
|
|
479
|
+
context: Union[dict, None, object] = _CONTEXT_NOT_PROVIDED,
|
|
480
|
+
context_mode: Literal["replace", "append", "prepend"] = "append",
|
|
481
|
+
**kwargs,
|
|
416
482
|
) -> Any:
|
|
417
483
|
"""
|
|
418
484
|
Execute automatic agentic loop and return typed response.
|
|
@@ -422,6 +488,13 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
422
488
|
- str: Single user message (will be wrapped in messages array)
|
|
423
489
|
- List[Dict[str, Any]]: Full conversation history with messages
|
|
424
490
|
in format [{"role": "user|assistant|system", "content": "..."}]
|
|
491
|
+
context: Optional runtime context for system prompt template rendering.
|
|
492
|
+
Can be dict, MeshContextModel, or None. If not provided,
|
|
493
|
+
uses the auto-populated context from decorator's context_param.
|
|
494
|
+
context_mode: How to merge runtime context with auto-populated context:
|
|
495
|
+
- "append" (default): auto_context | runtime_context (runtime wins on conflicts)
|
|
496
|
+
- "prepend": runtime_context | auto_context (auto wins on conflicts)
|
|
497
|
+
- "replace": use runtime_context entirely (ignores auto-populated)
|
|
425
498
|
**kwargs: Additional arguments passed to LLM
|
|
426
499
|
|
|
427
500
|
Returns:
|
|
@@ -431,6 +504,22 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
431
504
|
MaxIterationsError: If max iterations exceeded
|
|
432
505
|
ToolExecutionError: If tool execution fails
|
|
433
506
|
ValidationError: If response doesn't match output_type schema
|
|
507
|
+
|
|
508
|
+
Examples:
|
|
509
|
+
# Use auto-populated context (default behavior)
|
|
510
|
+
result = await llm("What is the answer?")
|
|
511
|
+
|
|
512
|
+
# Append extra context (runtime wins on key conflicts)
|
|
513
|
+
result = await llm("What is the answer?", context={"extra": "info"})
|
|
514
|
+
|
|
515
|
+
# Prepend context (auto wins on key conflicts)
|
|
516
|
+
result = await llm("What is the answer?", context={"extra": "info"}, context_mode="prepend")
|
|
517
|
+
|
|
518
|
+
# Replace context entirely
|
|
519
|
+
result = await llm("What is the answer?", context={"only": "this"}, context_mode="replace")
|
|
520
|
+
|
|
521
|
+
# Explicitly clear context
|
|
522
|
+
result = await llm("What is the answer?", context={}, context_mode="replace")
|
|
434
523
|
"""
|
|
435
524
|
self._iteration_count = 0
|
|
436
525
|
|
|
@@ -440,8 +529,11 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
440
529
|
"litellm is required for MeshLlmAgent. Install with: pip install litellm"
|
|
441
530
|
)
|
|
442
531
|
|
|
443
|
-
#
|
|
444
|
-
|
|
532
|
+
# Resolve effective context (merge auto-populated with runtime context)
|
|
533
|
+
effective_context = self._resolve_context(context, context_mode)
|
|
534
|
+
|
|
535
|
+
# Render base system prompt (from template or literal) with effective context
|
|
536
|
+
base_system_prompt = self._render_system_prompt(effective_context)
|
|
445
537
|
|
|
446
538
|
# Phase 2: Use provider handler to format system prompt
|
|
447
539
|
# This allows vendor-specific optimizations (e.g., OpenAI skips JSON instructions)
|
|
@@ -29,7 +29,7 @@ class UnifiedMCPProxy:
|
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
31
|
def __init__(
|
|
32
|
-
self, endpoint: str, function_name: str, kwargs_config:
|
|
32
|
+
self, endpoint: str, function_name: str, kwargs_config: dict | None = None
|
|
33
33
|
):
|
|
34
34
|
"""Initialize Unified MCP Proxy.
|
|
35
35
|
|
|
@@ -134,12 +134,20 @@ class UnifiedMCPProxy:
|
|
|
134
134
|
"X-Trace-ID": current_trace.trace_id,
|
|
135
135
|
"X-Parent-Span": current_trace.span_id, # Current span becomes parent for downstream
|
|
136
136
|
}
|
|
137
|
+
self.logger.info(
|
|
138
|
+
f"🔗 TRACE_PROPAGATION: Injecting headers trace_id={current_trace.trace_id[:8]}... "
|
|
139
|
+
f"parent_span={current_trace.span_id[:8]}..."
|
|
140
|
+
)
|
|
137
141
|
return headers
|
|
138
142
|
else:
|
|
143
|
+
self.logger.warning("🔗 TRACE_PROPAGATION: No trace context available")
|
|
139
144
|
return {}
|
|
140
145
|
|
|
141
146
|
except Exception as e:
|
|
142
147
|
# Never fail MCP calls due to tracing issues
|
|
148
|
+
self.logger.warning(
|
|
149
|
+
f"🔗 TRACE_PROPAGATION: Exception getting trace context: {e}"
|
|
150
|
+
)
|
|
143
151
|
return {}
|
|
144
152
|
|
|
145
153
|
def _configure_from_kwargs(self):
|
|
@@ -826,7 +834,7 @@ class EnhancedUnifiedMCPProxy(UnifiedMCPProxy):
|
|
|
826
834
|
"""
|
|
827
835
|
|
|
828
836
|
def __init__(
|
|
829
|
-
self, endpoint: str, function_name: str, kwargs_config:
|
|
837
|
+
self, endpoint: str, function_name: str, kwargs_config: dict | None = None
|
|
830
838
|
):
|
|
831
839
|
"""Initialize Enhanced Unified MCP Proxy."""
|
|
832
840
|
super().__init__(endpoint, function_name, kwargs_config)
|
|
@@ -54,8 +54,8 @@ class APIDependencyResolutionStep(PipelineStep):
|
|
|
54
54
|
result.message = (
|
|
55
55
|
"No heartbeat response or registry wrapper - completed successfully"
|
|
56
56
|
)
|
|
57
|
-
self.logger.
|
|
58
|
-
"
|
|
57
|
+
self.logger.debug(
|
|
58
|
+
"No heartbeat response to process - this is normal for API services"
|
|
59
59
|
)
|
|
60
60
|
return result
|
|
61
61
|
|
|
@@ -85,20 +85,7 @@ class APIDependencyResolutionStep(PipelineStep):
|
|
|
85
85
|
# Log function registry status for debugging
|
|
86
86
|
injector = get_global_injector()
|
|
87
87
|
function_count = len(injector._function_registry)
|
|
88
|
-
self.logger.debug(
|
|
89
|
-
f"🔍 Function registry contains {function_count} functions:"
|
|
90
|
-
)
|
|
91
|
-
for func_id, wrapper_func in injector._function_registry.items():
|
|
92
|
-
original_func = getattr(wrapper_func, "_mesh_original_func", None)
|
|
93
|
-
func_name = original_func.__name__ if original_func else "unknown"
|
|
94
|
-
dependencies = getattr(wrapper_func, "_mesh_dependencies", [])
|
|
95
|
-
self.logger.debug(
|
|
96
|
-
f" 📋 {func_id} -> {func_name} (deps: {dependencies})"
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
self.logger.debug(
|
|
100
|
-
"🔗 API dependency resolution step completed using hash-based change detection"
|
|
101
|
-
)
|
|
88
|
+
self.logger.debug(f"Function registry contains {function_count} functions")
|
|
102
89
|
|
|
103
90
|
except Exception as e:
|
|
104
91
|
result.status = PipelineStatus.FAILED
|
|
@@ -206,32 +193,33 @@ class APIDependencyResolutionStep(PipelineStep):
|
|
|
206
193
|
|
|
207
194
|
if _last_api_dependency_hash is None:
|
|
208
195
|
if function_count > 0:
|
|
209
|
-
self.logger.
|
|
210
|
-
f"
|
|
196
|
+
self.logger.debug(
|
|
197
|
+
f"Initial API dependency state detected: {function_count} functions, {total_deps} dependencies"
|
|
211
198
|
)
|
|
212
199
|
else:
|
|
213
|
-
self.logger.
|
|
214
|
-
"
|
|
200
|
+
self.logger.debug(
|
|
201
|
+
"Initial API dependency state detected: no dependencies"
|
|
215
202
|
)
|
|
216
203
|
else:
|
|
217
|
-
self.logger.
|
|
218
|
-
f"
|
|
204
|
+
self.logger.debug(
|
|
205
|
+
f"API dependency state changed (hash: {_last_api_dependency_hash} → {current_hash})"
|
|
219
206
|
)
|
|
220
207
|
if function_count > 0:
|
|
221
|
-
self.logger.
|
|
222
|
-
f"
|
|
208
|
+
self.logger.debug(
|
|
209
|
+
f"Updating API dependencies for {function_count} functions ({total_deps} total dependencies)"
|
|
223
210
|
)
|
|
224
211
|
else:
|
|
225
|
-
self.logger.
|
|
226
|
-
"
|
|
212
|
+
self.logger.debug(
|
|
213
|
+
"Registry reports no API dependencies - unwiring all existing dependencies"
|
|
227
214
|
)
|
|
228
215
|
|
|
229
216
|
# Import here to avoid circular imports
|
|
230
217
|
from ...engine.dependency_injector import get_global_injector
|
|
231
|
-
from ...engine.full_mcp_proxy import
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
218
|
+
from ...engine.full_mcp_proxy import EnhancedFullMCPProxy, FullMCPProxy
|
|
219
|
+
from ...engine.mcp_client_proxy import (
|
|
220
|
+
EnhancedMCPClientProxy,
|
|
221
|
+
MCPClientProxy,
|
|
222
|
+
)
|
|
235
223
|
|
|
236
224
|
injector = get_global_injector()
|
|
237
225
|
|
|
@@ -267,16 +255,32 @@ class APIDependencyResolutionStep(PipelineStep):
|
|
|
267
255
|
for dep_key in keys_to_remove:
|
|
268
256
|
await injector.unregister_dependency(dep_key)
|
|
269
257
|
unwired_count += 1
|
|
270
|
-
self.logger.
|
|
271
|
-
f"
|
|
258
|
+
self.logger.debug(
|
|
259
|
+
f"Unwired API dependency '{dep_key}' (no longer reported by registry)"
|
|
272
260
|
)
|
|
273
261
|
|
|
274
262
|
# Step 3: Apply all dependency updates using positional indexing
|
|
275
263
|
updated_count = 0
|
|
276
264
|
for function_name, dependency_list in current_state.items():
|
|
265
|
+
# Check if function_name is a route path (METHOD:path format)
|
|
266
|
+
# Route paths contain "/" and look like "GET:/api/v1/benchmark-services"
|
|
267
|
+
is_route_path = "/" in function_name
|
|
268
|
+
|
|
277
269
|
# Map tool name to func_id (using mapping from Step 1)
|
|
270
|
+
# For route paths, use the route_id directly as it won't be in tool_name_to_func_id
|
|
278
271
|
func_id = tool_name_to_func_id.get(function_name, function_name)
|
|
279
272
|
|
|
273
|
+
# Get route wrapper if this is a route path
|
|
274
|
+
route_wrapper_info = None
|
|
275
|
+
if is_route_path:
|
|
276
|
+
route_wrapper_info = DecoratorRegistry.get_route_wrapper(
|
|
277
|
+
function_name
|
|
278
|
+
)
|
|
279
|
+
if not route_wrapper_info:
|
|
280
|
+
self.logger.warning(
|
|
281
|
+
f"No route wrapper found for '{function_name}' - dependency injection may fail"
|
|
282
|
+
)
|
|
283
|
+
|
|
280
284
|
for dep_index, dep_info in enumerate(dependency_list):
|
|
281
285
|
status = dep_info["status"]
|
|
282
286
|
endpoint = dep_info["endpoint"]
|
|
@@ -288,33 +292,21 @@ class APIDependencyResolutionStep(PipelineStep):
|
|
|
288
292
|
# Import here to avoid circular imports
|
|
289
293
|
import os
|
|
290
294
|
|
|
291
|
-
from ...engine.self_dependency_proxy import
|
|
292
|
-
|
|
293
|
-
from ...engine.unified_mcp_proxy import \
|
|
294
|
-
EnhancedUnifiedMCPProxy
|
|
295
|
+
from ...engine.self_dependency_proxy import SelfDependencyProxy
|
|
296
|
+
from ...engine.unified_mcp_proxy import EnhancedUnifiedMCPProxy
|
|
295
297
|
|
|
296
298
|
# Get current agent ID for self-dependency detection
|
|
297
299
|
current_agent_id = None
|
|
298
300
|
try:
|
|
299
|
-
from ...engine.decorator_registry import
|
|
300
|
-
DecoratorRegistry
|
|
301
|
+
from ...engine.decorator_registry import DecoratorRegistry
|
|
301
302
|
|
|
302
303
|
config = DecoratorRegistry.get_resolved_agent_config()
|
|
303
304
|
current_agent_id = config["agent_id"]
|
|
304
|
-
|
|
305
|
-
f"🔍 Current API service ID from DecoratorRegistry: '{current_agent_id}'"
|
|
306
|
-
)
|
|
307
|
-
except Exception as e:
|
|
305
|
+
except Exception:
|
|
308
306
|
# For API services, try environment variable fallback
|
|
309
307
|
current_agent_id = os.getenv("MCP_MESH_AGENT_ID")
|
|
310
|
-
self.logger.debug(
|
|
311
|
-
f"🔍 Current API service ID from environment: '{current_agent_id}' (fallback due to: {e})"
|
|
312
|
-
)
|
|
313
308
|
|
|
314
309
|
target_agent_id = dep_info.get("agent_id")
|
|
315
|
-
self.logger.debug(
|
|
316
|
-
f"🔍 Target agent ID from registry: '{target_agent_id}'"
|
|
317
|
-
)
|
|
318
310
|
|
|
319
311
|
# Determine if this is a self-dependency (less common for API services)
|
|
320
312
|
is_self_dependency = (
|
|
@@ -323,12 +315,6 @@ class APIDependencyResolutionStep(PipelineStep):
|
|
|
323
315
|
and current_agent_id == target_agent_id
|
|
324
316
|
)
|
|
325
317
|
|
|
326
|
-
self.logger.debug(
|
|
327
|
-
f"🔍 Self-dependency check for '{capability}': "
|
|
328
|
-
f"current='{current_agent_id}' vs target='{target_agent_id}' "
|
|
329
|
-
f"→ {'SELF' if is_self_dependency else 'CROSS'}-dependency"
|
|
330
|
-
)
|
|
331
|
-
|
|
332
318
|
if is_self_dependency:
|
|
333
319
|
# Create self-dependency proxy with WRAPPER function (not original)
|
|
334
320
|
# The wrapper has dependency injection logic, so calling it ensures
|
|
@@ -336,16 +322,13 @@ class APIDependencyResolutionStep(PipelineStep):
|
|
|
336
322
|
wrapper_func = None
|
|
337
323
|
if dep_function_name in mesh_tools:
|
|
338
324
|
wrapper_func = mesh_tools[dep_function_name].function
|
|
339
|
-
self.logger.debug(
|
|
340
|
-
f"🔍 Found wrapper for '{dep_function_name}' in DecoratorRegistry"
|
|
341
|
-
)
|
|
342
325
|
|
|
343
326
|
if wrapper_func:
|
|
344
327
|
new_proxy = SelfDependencyProxy(
|
|
345
328
|
wrapper_func, dep_function_name
|
|
346
329
|
)
|
|
347
|
-
self.logger.
|
|
348
|
-
f"
|
|
330
|
+
self.logger.debug(
|
|
331
|
+
f"API SELF-DEPENDENCY: Using wrapper for '{capability}' "
|
|
349
332
|
f"(local call with full DI support)"
|
|
350
333
|
)
|
|
351
334
|
else:
|
|
@@ -380,57 +363,57 @@ class APIDependencyResolutionStep(PipelineStep):
|
|
|
380
363
|
kwargs_config=kwargs_config,
|
|
381
364
|
)
|
|
382
365
|
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
366
|
+
# For route paths, directly update the wrapper's dependencies
|
|
367
|
+
# This bypasses the injector key-based lookup which doesn't work for routes
|
|
368
|
+
if route_wrapper_info:
|
|
369
|
+
wrapper = route_wrapper_info.get("wrapper")
|
|
370
|
+
if wrapper and hasattr(wrapper, "_mesh_update_dependency"):
|
|
371
|
+
wrapper._mesh_update_dependency(dep_index, new_proxy)
|
|
372
|
+
updated_count += 1
|
|
373
|
+
self.logger.debug(
|
|
374
|
+
f"Updated route dependency '{capability}' at position {dep_index} "
|
|
375
|
+
f"→ {endpoint}/{dep_function_name} for route '{function_name}'"
|
|
376
|
+
)
|
|
377
|
+
else:
|
|
378
|
+
self.logger.warning(
|
|
379
|
+
f"Route wrapper for '{function_name}' doesn't have _mesh_update_dependency method"
|
|
380
|
+
)
|
|
381
|
+
else:
|
|
382
|
+
# Fallback: Register with composite key using func_id for MCP tools
|
|
383
|
+
dep_key = f"{func_id}:dep_{dep_index}"
|
|
384
|
+
await injector.register_dependency(dep_key, new_proxy)
|
|
385
|
+
updated_count += 1
|
|
386
|
+
|
|
387
|
+
# Log which functions will be affected
|
|
388
|
+
affected_functions = injector._dependency_mapping.get(
|
|
389
|
+
dep_key, set()
|
|
390
|
+
)
|
|
391
|
+
self.logger.debug(
|
|
392
|
+
f"Functions affected by '{capability}' at position {dep_index}: {list(affected_functions)}"
|
|
393
|
+
)
|
|
398
394
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
)
|
|
395
|
+
self.logger.debug(
|
|
396
|
+
f"Updated API dependency '{capability}' at position {dep_index} → {endpoint}/{dep_function_name}"
|
|
397
|
+
)
|
|
398
|
+
self.logger.debug(
|
|
399
|
+
f"Registered dependency '{capability}' at position {dep_index} with key '{dep_key}' (func_id: {func_id})"
|
|
400
|
+
)
|
|
406
401
|
else:
|
|
407
402
|
if status != "available":
|
|
408
403
|
self.logger.debug(
|
|
409
|
-
f"
|
|
404
|
+
f"API dependency '{capability}' at position {dep_index} not available: {status}"
|
|
410
405
|
)
|
|
411
406
|
else:
|
|
412
407
|
self.logger.warning(
|
|
413
|
-
f"
|
|
408
|
+
f"Cannot update API dependency '{capability}' at position {dep_index}: missing endpoint or function_name"
|
|
414
409
|
)
|
|
415
410
|
|
|
416
411
|
# Store new hash for next comparison (use global variable)
|
|
417
412
|
_last_api_dependency_hash = current_hash
|
|
418
413
|
|
|
419
|
-
if unwired_count > 0
|
|
420
|
-
self.logger.
|
|
421
|
-
f"
|
|
422
|
-
)
|
|
423
|
-
elif unwired_count > 0:
|
|
424
|
-
self.logger.info(
|
|
425
|
-
f"✅ Successfully unwired {unwired_count} API dependencies (state hash: {current_hash})"
|
|
426
|
-
)
|
|
427
|
-
elif updated_count > 0:
|
|
428
|
-
self.logger.info(
|
|
429
|
-
f"✅ Successfully updated {updated_count} API dependencies (state hash: {current_hash})"
|
|
430
|
-
)
|
|
431
|
-
else:
|
|
432
|
-
self.logger.info(
|
|
433
|
-
f"✅ API dependency state synchronized (state hash: {current_hash})"
|
|
414
|
+
if unwired_count > 0 or updated_count > 0:
|
|
415
|
+
self.logger.debug(
|
|
416
|
+
f"API dependency sync: unwired={unwired_count}, updated={updated_count} (hash: {current_hash})"
|
|
434
417
|
)
|
|
435
418
|
|
|
436
419
|
except Exception as e:
|
|
@@ -482,8 +465,7 @@ class APIDependencyResolutionStep(PipelineStep):
|
|
|
482
465
|
Proxy instance
|
|
483
466
|
"""
|
|
484
467
|
from ...engine.full_mcp_proxy import EnhancedFullMCPProxy, FullMCPProxy
|
|
485
|
-
from ...engine.mcp_client_proxy import
|
|
486
|
-
MCPClientProxy)
|
|
468
|
+
from ...engine.mcp_client_proxy import EnhancedMCPClientProxy, MCPClientProxy
|
|
487
469
|
|
|
488
470
|
if proxy_type == "FullMCPProxy":
|
|
489
471
|
# Use enhanced proxy if kwargs available
|