mcp-mesh 0.7.12__py3-none-any.whl → 0.7.13__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/__init__.py +1 -22
- _mcp_mesh/engine/async_mcp_client.py +88 -25
- _mcp_mesh/engine/decorator_registry.py +10 -9
- _mcp_mesh/engine/dependency_injector.py +64 -53
- _mcp_mesh/engine/mesh_llm_agent.py +119 -5
- _mcp_mesh/engine/mesh_llm_agent_injector.py +30 -0
- _mcp_mesh/engine/session_aware_client.py +3 -3
- _mcp_mesh/engine/unified_mcp_proxy.py +82 -90
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -89
- _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +3 -3
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +30 -28
- _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +16 -18
- _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +5 -5
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +3 -3
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +6 -6
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +1 -1
- _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +15 -11
- _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +3 -3
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +37 -268
- _mcp_mesh/pipeline/mcp_startup/lifespan_factory.py +142 -0
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +57 -93
- _mcp_mesh/pipeline/shared/registry_connection.py +1 -1
- _mcp_mesh/shared/health_check_manager.py +313 -0
- _mcp_mesh/shared/logging_config.py +190 -7
- _mcp_mesh/shared/registry_client_wrapper.py +8 -8
- _mcp_mesh/shared/sse_parser.py +19 -17
- _mcp_mesh/tracing/execution_tracer.py +26 -1
- _mcp_mesh/tracing/fastapi_tracing_middleware.py +3 -4
- _mcp_mesh/tracing/trace_context_helper.py +25 -6
- {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.13.dist-info}/METADATA +1 -1
- {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.13.dist-info}/RECORD +38 -39
- mesh/__init__.py +3 -1
- mesh/decorators.py +81 -43
- mesh/helpers.py +72 -4
- mesh/types.py +48 -4
- _mcp_mesh/engine/full_mcp_proxy.py +0 -641
- _mcp_mesh/engine/mcp_client_proxy.py +0 -457
- _mcp_mesh/shared/health_check_cache.py +0 -246
- {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.13.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,6 +7,7 @@ Provides automatic agentic loop for LLM-based agents with tool integration.
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import json
|
|
9
9
|
import logging
|
|
10
|
+
import time
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Any, Dict, List, Literal, Optional, Union
|
|
12
13
|
|
|
@@ -67,6 +68,7 @@ class MeshLlmAgent:
|
|
|
67
68
|
context_value: Optional[Any] = None,
|
|
68
69
|
provider_proxy: Optional[Any] = None,
|
|
69
70
|
vendor: Optional[str] = None,
|
|
71
|
+
default_model_params: Optional[dict[str, Any]] = None,
|
|
70
72
|
):
|
|
71
73
|
"""
|
|
72
74
|
Initialize MeshLlmAgent proxy.
|
|
@@ -80,6 +82,9 @@ class MeshLlmAgent:
|
|
|
80
82
|
context_value: Optional context for template rendering (MeshContextModel, dict, or None)
|
|
81
83
|
provider_proxy: Optional pre-resolved provider proxy for mesh delegation
|
|
82
84
|
vendor: Optional vendor name for handler selection (e.g., "anthropic", "openai")
|
|
85
|
+
default_model_params: Optional dict of default LLM parameters from decorator
|
|
86
|
+
(e.g., max_tokens, temperature). These are merged with
|
|
87
|
+
call-time kwargs, with call-time taking precedence.
|
|
83
88
|
"""
|
|
84
89
|
self.config = config
|
|
85
90
|
self.provider = config.provider
|
|
@@ -92,6 +97,9 @@ class MeshLlmAgent:
|
|
|
92
97
|
self.system_prompt = config.system_prompt # Public attribute for tests
|
|
93
98
|
self.output_mode = config.output_mode # Output mode override (strict/hint/text)
|
|
94
99
|
self._iteration_count = 0
|
|
100
|
+
self._default_model_params = (
|
|
101
|
+
default_model_params or {}
|
|
102
|
+
) # Decorator-level defaults
|
|
95
103
|
|
|
96
104
|
# Detect if using mesh delegation (provider is dict)
|
|
97
105
|
self._is_mesh_delegated = isinstance(self.provider, dict)
|
|
@@ -336,6 +344,64 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
336
344
|
# Otherwise, use literal system prompt from config
|
|
337
345
|
return self.system_prompt or ""
|
|
338
346
|
|
|
347
|
+
def _attach_mesh_meta(
|
|
348
|
+
self,
|
|
349
|
+
result: Any,
|
|
350
|
+
model: str,
|
|
351
|
+
input_tokens: int,
|
|
352
|
+
output_tokens: int,
|
|
353
|
+
latency_ms: float,
|
|
354
|
+
) -> Any:
|
|
355
|
+
"""
|
|
356
|
+
Attach _mesh_meta to result object if possible.
|
|
357
|
+
|
|
358
|
+
For Pydantic models and regular classes, attaches LlmMeta as _mesh_meta.
|
|
359
|
+
For primitives (str, int, etc.) and frozen models, silently skips.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
result: The parsed result object
|
|
363
|
+
model: Model identifier used
|
|
364
|
+
input_tokens: Total input tokens across all iterations
|
|
365
|
+
output_tokens: Total output tokens across all iterations
|
|
366
|
+
latency_ms: Total latency in milliseconds
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
The result object (unchanged, but with _mesh_meta attached if possible)
|
|
370
|
+
"""
|
|
371
|
+
from mesh.types import LlmMeta
|
|
372
|
+
|
|
373
|
+
# Extract provider from model string (e.g., "anthropic/claude-3-5-haiku" -> "anthropic")
|
|
374
|
+
provider = "unknown"
|
|
375
|
+
if isinstance(model, str) and "/" in model:
|
|
376
|
+
provider = model.split("/")[0]
|
|
377
|
+
|
|
378
|
+
meta = LlmMeta(
|
|
379
|
+
provider=provider,
|
|
380
|
+
model=model or "unknown",
|
|
381
|
+
input_tokens=input_tokens,
|
|
382
|
+
output_tokens=output_tokens,
|
|
383
|
+
total_tokens=input_tokens + output_tokens,
|
|
384
|
+
latency_ms=latency_ms,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Try to attach _mesh_meta to result
|
|
388
|
+
try:
|
|
389
|
+
# This works for Pydantic models and most Python objects
|
|
390
|
+
object.__setattr__(result, "_mesh_meta", meta)
|
|
391
|
+
logger.debug(
|
|
392
|
+
f"📊 Attached _mesh_meta: model={model}, "
|
|
393
|
+
f"tokens={input_tokens}+{output_tokens}={input_tokens + output_tokens}, "
|
|
394
|
+
f"latency={latency_ms:.1f}ms"
|
|
395
|
+
)
|
|
396
|
+
except (TypeError, AttributeError):
|
|
397
|
+
# Primitives (str, int, etc.) and frozen objects don't support attribute assignment
|
|
398
|
+
logger.debug(
|
|
399
|
+
f"📊 Could not attach _mesh_meta to {type(result).__name__} "
|
|
400
|
+
f"(tokens={input_tokens}+{output_tokens}, latency={latency_ms:.1f}ms)"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
return result
|
|
404
|
+
|
|
339
405
|
async def _get_mesh_provider(self) -> Any:
|
|
340
406
|
"""
|
|
341
407
|
Get the mesh provider proxy (already resolved during heartbeat).
|
|
@@ -456,9 +522,20 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
456
522
|
self.message = message
|
|
457
523
|
self.finish_reason = "stop"
|
|
458
524
|
|
|
525
|
+
# Issue #311: Mock usage object for token tracking
|
|
526
|
+
class MockUsage:
|
|
527
|
+
def __init__(self, usage_dict):
|
|
528
|
+
self.prompt_tokens = usage_dict.get("prompt_tokens", 0)
|
|
529
|
+
self.completion_tokens = usage_dict.get("completion_tokens", 0)
|
|
530
|
+
self.total_tokens = self.prompt_tokens + self.completion_tokens
|
|
531
|
+
|
|
459
532
|
class MockResponse:
|
|
460
533
|
def __init__(self, message_dict):
|
|
461
534
|
self.choices = [MockChoice(MockMessage(message_dict))]
|
|
535
|
+
# Issue #311: Extract usage from _mesh_usage if present
|
|
536
|
+
mesh_usage = message_dict.get("_mesh_usage")
|
|
537
|
+
self.usage = MockUsage(mesh_usage) if mesh_usage else None
|
|
538
|
+
self.model = mesh_usage.get("model") if mesh_usage else None
|
|
462
539
|
|
|
463
540
|
logger.debug(
|
|
464
541
|
f"📥 Received response from mesh provider: "
|
|
@@ -523,6 +600,12 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
523
600
|
"""
|
|
524
601
|
self._iteration_count = 0
|
|
525
602
|
|
|
603
|
+
# Issue #311: Track timing and token usage for _mesh_meta
|
|
604
|
+
start_time = time.perf_counter()
|
|
605
|
+
total_input_tokens = 0
|
|
606
|
+
total_output_tokens = 0
|
|
607
|
+
effective_model = self.model # Track actual model used
|
|
608
|
+
|
|
526
609
|
# Check if litellm is available
|
|
527
610
|
if completion is None:
|
|
528
611
|
raise ImportError(
|
|
@@ -583,11 +666,15 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
583
666
|
try:
|
|
584
667
|
# Call LLM (either direct LiteLLM or mesh-delegated)
|
|
585
668
|
try:
|
|
669
|
+
# Merge decorator-level defaults with call-time kwargs
|
|
670
|
+
# Call-time kwargs take precedence over defaults
|
|
671
|
+
effective_kwargs = {**self._default_model_params, **kwargs}
|
|
672
|
+
|
|
586
673
|
# Build kwargs with output_mode override if set
|
|
587
674
|
call_kwargs = (
|
|
588
|
-
{**
|
|
675
|
+
{**effective_kwargs, "output_mode": self.output_mode}
|
|
589
676
|
if self.output_mode
|
|
590
|
-
else
|
|
677
|
+
else effective_kwargs
|
|
591
678
|
)
|
|
592
679
|
|
|
593
680
|
# Use provider handler to prepare vendor-specific request
|
|
@@ -600,7 +687,7 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
600
687
|
|
|
601
688
|
if self._is_mesh_delegated:
|
|
602
689
|
# Mesh delegation: extract model_params to send to provider
|
|
603
|
-
# Exclude messages/tools (separate params),
|
|
690
|
+
# Exclude messages/tools (separate params), api_key (provider has it),
|
|
604
691
|
# and output_mode (only used locally by prepare_request)
|
|
605
692
|
model_params = {
|
|
606
693
|
k: v
|
|
@@ -609,12 +696,18 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
609
696
|
not in [
|
|
610
697
|
"messages",
|
|
611
698
|
"tools",
|
|
612
|
-
"model",
|
|
613
699
|
"api_key",
|
|
614
700
|
"output_mode",
|
|
701
|
+
"model", # Model handled separately below
|
|
615
702
|
]
|
|
616
703
|
}
|
|
617
704
|
|
|
705
|
+
# Issue #308: Include model override if explicitly set by consumer
|
|
706
|
+
# This allows consumer to request a specific model from the provider
|
|
707
|
+
# (e.g., use haiku instead of provider's default sonnet)
|
|
708
|
+
if self.model:
|
|
709
|
+
model_params["model"] = self.model
|
|
710
|
+
|
|
618
711
|
logger.debug(
|
|
619
712
|
f"📤 Delegating to mesh provider with handler-prepared params: "
|
|
620
713
|
f"keys={list(model_params.keys())}"
|
|
@@ -645,6 +738,16 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
645
738
|
original_error=e,
|
|
646
739
|
) from e
|
|
647
740
|
|
|
741
|
+
# Issue #311: Extract token usage from response
|
|
742
|
+
if hasattr(response, "usage") and response.usage:
|
|
743
|
+
usage = response.usage
|
|
744
|
+
total_input_tokens += getattr(usage, "prompt_tokens", 0) or 0
|
|
745
|
+
total_output_tokens += getattr(usage, "completion_tokens", 0) or 0
|
|
746
|
+
|
|
747
|
+
# Issue #311: Track effective model (may differ from requested in mesh delegation)
|
|
748
|
+
if hasattr(response, "model") and response.model:
|
|
749
|
+
effective_model = response.model
|
|
750
|
+
|
|
648
751
|
# Extract response content
|
|
649
752
|
assistant_message = response.choices[0].message
|
|
650
753
|
|
|
@@ -675,7 +778,18 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
675
778
|
f"📥 Raw LLM response: {assistant_message.content[:500]}..."
|
|
676
779
|
)
|
|
677
780
|
|
|
678
|
-
|
|
781
|
+
# Parse the response
|
|
782
|
+
result = self._parse_response(assistant_message.content)
|
|
783
|
+
|
|
784
|
+
# Issue #311: Calculate latency and attach _mesh_meta
|
|
785
|
+
latency_ms = (time.perf_counter() - start_time) * 1000
|
|
786
|
+
return self._attach_mesh_meta(
|
|
787
|
+
result=result,
|
|
788
|
+
model=effective_model,
|
|
789
|
+
input_tokens=total_input_tokens,
|
|
790
|
+
output_tokens=total_output_tokens,
|
|
791
|
+
latency_ms=latency_ms,
|
|
792
|
+
)
|
|
679
793
|
|
|
680
794
|
except LLMAPIError:
|
|
681
795
|
# Re-raise LLM API errors as-is
|
|
@@ -537,6 +537,30 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
537
537
|
is_template = config_dict.get("is_template", False)
|
|
538
538
|
template_path = config_dict.get("template_path")
|
|
539
539
|
|
|
540
|
+
# Extract model params (everything except internal config keys)
|
|
541
|
+
# These are passed through to LiteLLM (e.g., max_tokens, temperature)
|
|
542
|
+
INTERNAL_CONFIG_KEYS = {
|
|
543
|
+
"filter",
|
|
544
|
+
"filter_mode",
|
|
545
|
+
"provider",
|
|
546
|
+
"model",
|
|
547
|
+
"api_key",
|
|
548
|
+
"max_iterations",
|
|
549
|
+
"system_prompt",
|
|
550
|
+
"system_prompt_file",
|
|
551
|
+
"is_template",
|
|
552
|
+
"template_path",
|
|
553
|
+
"context_param",
|
|
554
|
+
"output_mode",
|
|
555
|
+
}
|
|
556
|
+
default_model_params = {
|
|
557
|
+
k: v for k, v in config_dict.items() if k not in INTERNAL_CONFIG_KEYS
|
|
558
|
+
}
|
|
559
|
+
if default_model_params:
|
|
560
|
+
logger.debug(
|
|
561
|
+
f"🔧 Extracted default model params for {function_id}: {list(default_model_params.keys())}"
|
|
562
|
+
)
|
|
563
|
+
|
|
540
564
|
# Determine vendor for provider handler selection
|
|
541
565
|
# Priority: 1) From registry (mesh delegation), 2) From model name, 3) None
|
|
542
566
|
vendor = llm_agent_data.get("vendor")
|
|
@@ -564,6 +588,7 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
564
588
|
"provider_proxy"
|
|
565
589
|
), # Provider proxy for mesh delegation
|
|
566
590
|
vendor=vendor, # Vendor for provider handler selection (from registry or model name)
|
|
591
|
+
default_model_params=default_model_params, # Decorator-level LLM params
|
|
567
592
|
)
|
|
568
593
|
|
|
569
594
|
logger.debug(
|
|
@@ -574,6 +599,11 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
574
599
|
if isinstance(config_dict.get("provider"), dict)
|
|
575
600
|
else ""
|
|
576
601
|
)
|
|
602
|
+
+ (
|
|
603
|
+
f", model_params={list(default_model_params.keys())}"
|
|
604
|
+
if default_model_params
|
|
605
|
+
else ""
|
|
606
|
+
)
|
|
577
607
|
)
|
|
578
608
|
|
|
579
609
|
return llm_agent
|
|
@@ -11,8 +11,8 @@ import logging
|
|
|
11
11
|
import random
|
|
12
12
|
from typing import Any, Dict, List, Optional
|
|
13
13
|
|
|
14
|
-
from .mcp_client_proxy import MCPClientProxy
|
|
15
14
|
from .session_manager import get_session_manager
|
|
15
|
+
from .unified_mcp_proxy import UnifiedMCPProxy
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
@@ -72,10 +72,10 @@ class SessionAwareMCPClient:
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
# Create client proxy and make the call
|
|
75
|
-
|
|
75
|
+
proxy = UnifiedMCPProxy(target_agent["endpoint"], capability)
|
|
76
76
|
|
|
77
77
|
try:
|
|
78
|
-
result = await
|
|
78
|
+
result = await proxy(**arguments)
|
|
79
79
|
|
|
80
80
|
# Update session affinity if this was a session-required call
|
|
81
81
|
if session_id and routing_strategy.get("session_required"):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Unified MCP Proxy using FastMCP's built-in client.
|
|
2
2
|
|
|
3
|
-
This
|
|
4
|
-
|
|
3
|
+
This is the primary MCP client proxy for cross-service communication,
|
|
4
|
+
using FastMCP's superior client capabilities with async support.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
@@ -10,6 +10,13 @@ import uuid
|
|
|
10
10
|
from collections.abc import AsyncIterator
|
|
11
11
|
from typing import Any, Optional
|
|
12
12
|
|
|
13
|
+
from ..shared.logging_config import (
|
|
14
|
+
format_log_value,
|
|
15
|
+
format_result_summary,
|
|
16
|
+
get_trace_prefix,
|
|
17
|
+
)
|
|
18
|
+
from ..shared.sse_parser import SSEParser
|
|
19
|
+
|
|
13
20
|
logger = logging.getLogger(__name__)
|
|
14
21
|
|
|
15
22
|
|
|
@@ -227,9 +234,6 @@ class UnifiedMCPProxy:
|
|
|
227
234
|
self.collect_performance_metrics = self.kwargs_config.get(
|
|
228
235
|
"performance_metrics", True
|
|
229
236
|
)
|
|
230
|
-
self.include_telemetry_in_response = self.kwargs_config.get(
|
|
231
|
-
"include_telemetry_response", True
|
|
232
|
-
)
|
|
233
237
|
|
|
234
238
|
# Agent context collection
|
|
235
239
|
self.collect_agent_context = self.kwargs_config.get(
|
|
@@ -397,12 +401,22 @@ class UnifiedMCPProxy:
|
|
|
397
401
|
|
|
398
402
|
start_time = time.time()
|
|
399
403
|
|
|
404
|
+
# Get trace prefix if available
|
|
405
|
+
tp = get_trace_prefix()
|
|
406
|
+
|
|
407
|
+
# Log cross-agent call - summary line
|
|
408
|
+
arg_keys = list(arguments.keys()) if arguments else []
|
|
409
|
+
self.logger.debug(
|
|
410
|
+
f"{tp}🔄 Cross-agent call: {self.endpoint}/{name} (timeout: {self.timeout}s, args={arg_keys})"
|
|
411
|
+
)
|
|
412
|
+
# Log full args (will be TRACE later)
|
|
413
|
+
self.logger.debug(
|
|
414
|
+
f"{tp}🔄 Cross-agent call args: {format_log_value(arguments)}"
|
|
415
|
+
)
|
|
416
|
+
|
|
400
417
|
try:
|
|
401
418
|
# Use correct FastMCP client endpoint - agents expose MCP on /mcp
|
|
402
419
|
mcp_endpoint = f"{self.endpoint}/mcp"
|
|
403
|
-
self.logger.debug(f"🔄 Trying FastMCP client with endpoint: {mcp_endpoint}")
|
|
404
|
-
|
|
405
|
-
# Create client with automatic trace header injection
|
|
406
420
|
client_instance = self._create_fastmcp_client(mcp_endpoint)
|
|
407
421
|
|
|
408
422
|
async with client_instance as client:
|
|
@@ -422,21 +436,13 @@ class UnifiedMCPProxy:
|
|
|
422
436
|
# Convert CallToolResult to native Python structures for client simplicity
|
|
423
437
|
converted_result = self._convert_mcp_result_to_python(result)
|
|
424
438
|
|
|
425
|
-
#
|
|
426
|
-
if self.include_telemetry_in_response and isinstance(
|
|
427
|
-
converted_result, dict
|
|
428
|
-
):
|
|
429
|
-
converted_result["_telemetry"] = {
|
|
430
|
-
"method": "fastmcp_client",
|
|
431
|
-
"duration_ms": duration_ms,
|
|
432
|
-
"endpoint": mcp_endpoint,
|
|
433
|
-
"tool_name": name,
|
|
434
|
-
"telemetry_enabled": self.telemetry_enabled,
|
|
435
|
-
"distributed_tracing": self.distributed_tracing_enabled,
|
|
436
|
-
}
|
|
437
|
-
|
|
439
|
+
# Log success - summary line
|
|
438
440
|
self.logger.info(
|
|
439
|
-
f"✅ FastMCP tool call successful: {name} in {duration_ms}ms"
|
|
441
|
+
f"{tp}✅ FastMCP tool call successful: {name} in {duration_ms}ms → {format_result_summary(converted_result)}"
|
|
442
|
+
)
|
|
443
|
+
# Log full result (will be TRACE later)
|
|
444
|
+
self.logger.debug(
|
|
445
|
+
f"{tp}🔄 Cross-agent call result: {format_log_value(converted_result)}"
|
|
440
446
|
)
|
|
441
447
|
return converted_result
|
|
442
448
|
|
|
@@ -556,6 +562,52 @@ class UnifiedMCPProxy:
|
|
|
556
562
|
else:
|
|
557
563
|
return {"data": str(structured_content)}
|
|
558
564
|
|
|
565
|
+
def _normalize_http_result(self, result: Any) -> Any:
|
|
566
|
+
"""Normalize HTTP fallback result to match FastMCP format.
|
|
567
|
+
|
|
568
|
+
Extracts structured content from MCP envelope so callers get consistent
|
|
569
|
+
response format regardless of transport method (FastMCP vs HTTP fallback).
|
|
570
|
+
"""
|
|
571
|
+
if not isinstance(result, dict):
|
|
572
|
+
return result
|
|
573
|
+
|
|
574
|
+
# If result has "content" array (MCP envelope format), extract the data
|
|
575
|
+
if "content" in result and isinstance(result["content"], list):
|
|
576
|
+
content_list = result["content"]
|
|
577
|
+
|
|
578
|
+
if not content_list:
|
|
579
|
+
return None
|
|
580
|
+
|
|
581
|
+
# Single content item - extract and parse
|
|
582
|
+
if len(content_list) == 1:
|
|
583
|
+
content_item = content_list[0]
|
|
584
|
+
if isinstance(content_item, dict) and "text" in content_item:
|
|
585
|
+
# Try to parse JSON text content
|
|
586
|
+
try:
|
|
587
|
+
import json
|
|
588
|
+
|
|
589
|
+
return json.loads(content_item["text"])
|
|
590
|
+
except (json.JSONDecodeError, TypeError):
|
|
591
|
+
return content_item["text"]
|
|
592
|
+
return content_item
|
|
593
|
+
|
|
594
|
+
# Multiple content items - convert each
|
|
595
|
+
converted_items = []
|
|
596
|
+
for item in content_list:
|
|
597
|
+
if isinstance(item, dict) and "text" in item:
|
|
598
|
+
try:
|
|
599
|
+
import json
|
|
600
|
+
|
|
601
|
+
converted_items.append(json.loads(item["text"]))
|
|
602
|
+
except (json.JSONDecodeError, TypeError):
|
|
603
|
+
converted_items.append(item["text"])
|
|
604
|
+
else:
|
|
605
|
+
converted_items.append(item)
|
|
606
|
+
return {"content": converted_items, "type": "multi_content"}
|
|
607
|
+
|
|
608
|
+
# Result is already in clean format
|
|
609
|
+
return result
|
|
610
|
+
|
|
559
611
|
async def _fallback_http_call(self, name: str, arguments: dict = None) -> Any:
|
|
560
612
|
"""Enhanced fallback HTTP call using httpx directly with performance tracking."""
|
|
561
613
|
import time
|
|
@@ -614,41 +666,10 @@ class UnifiedMCPProxy:
|
|
|
614
666
|
f"📄 Response length: {len(response_text)} chars, starts with: {response_text[:100]}"
|
|
615
667
|
)
|
|
616
668
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
self.logger.debug("🔄 Parsing SSE format response")
|
|
622
|
-
# Parse SSE format - handle multiple events
|
|
623
|
-
for line in response_text.split("\n"):
|
|
624
|
-
line = line.strip()
|
|
625
|
-
if line.startswith("data:"):
|
|
626
|
-
json_str = line[5:].strip()
|
|
627
|
-
if json_str and json_str != "":
|
|
628
|
-
try:
|
|
629
|
-
data = json.loads(json_str)
|
|
630
|
-
self.logger.debug(
|
|
631
|
-
f"✅ Successfully parsed SSE data: {type(data)}"
|
|
632
|
-
)
|
|
633
|
-
break
|
|
634
|
-
except json.JSONDecodeError as e:
|
|
635
|
-
self.logger.warning(
|
|
636
|
-
f"⚠️ Failed to parse SSE line: {json_str[:100]}, error: {e}"
|
|
637
|
-
)
|
|
638
|
-
continue
|
|
639
|
-
else:
|
|
640
|
-
# Plain JSON response
|
|
641
|
-
self.logger.debug("🔄 Parsing plain JSON response")
|
|
642
|
-
try:
|
|
643
|
-
data = json.loads(response_text)
|
|
644
|
-
except json.JSONDecodeError as e:
|
|
645
|
-
self.logger.error(
|
|
646
|
-
f"❌ Failed to parse JSON response: {e}, content: {response_text[:200]}"
|
|
647
|
-
)
|
|
648
|
-
raise RuntimeError(f"Invalid JSON response: {e}")
|
|
649
|
-
|
|
650
|
-
if data is None:
|
|
651
|
-
raise RuntimeError("No valid data found in response")
|
|
669
|
+
# Use shared SSE parser for both SSE and plain JSON responses
|
|
670
|
+
data = SSEParser.parse_sse_response(
|
|
671
|
+
response_text, f"UnifiedMCPProxy.{name}"
|
|
672
|
+
)
|
|
652
673
|
|
|
653
674
|
# Check for JSON-RPC error
|
|
654
675
|
if "error" in data:
|
|
@@ -673,39 +694,10 @@ class UnifiedMCPProxy:
|
|
|
673
694
|
f"📊 HTTP fallback performance: {duration_ms}ms for tool '{name}'"
|
|
674
695
|
)
|
|
675
696
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
# Add performance metadata to result if enabled
|
|
681
|
-
if self.include_telemetry_in_response and isinstance(result, dict):
|
|
682
|
-
result["_telemetry"] = {
|
|
683
|
-
"method": "http_fallback",
|
|
684
|
-
"duration_ms": duration_ms,
|
|
685
|
-
"endpoint": self.endpoint,
|
|
686
|
-
"tool_name": name,
|
|
687
|
-
"telemetry_enabled": self.telemetry_enabled,
|
|
688
|
-
"fallback_reason": "fastmcp_unavailable",
|
|
689
|
-
}
|
|
690
|
-
return result
|
|
691
|
-
else:
|
|
692
|
-
# Wrap in CallToolResult-like structure with performance data
|
|
693
|
-
self.logger.info(
|
|
694
|
-
f"🔄 Wrapping result in CallToolResult structure ({duration_ms}ms)"
|
|
695
|
-
)
|
|
696
|
-
wrapped_result = {
|
|
697
|
-
"content": [{"type": "text", "text": str(result)}]
|
|
698
|
-
}
|
|
699
|
-
if self.include_telemetry_in_response:
|
|
700
|
-
wrapped_result["_telemetry"] = {
|
|
701
|
-
"method": "http_fallback",
|
|
702
|
-
"duration_ms": duration_ms,
|
|
703
|
-
"endpoint": self.endpoint,
|
|
704
|
-
"tool_name": name,
|
|
705
|
-
"telemetry_enabled": self.telemetry_enabled,
|
|
706
|
-
"fallback_reason": "fastmcp_unavailable",
|
|
707
|
-
}
|
|
708
|
-
return wrapped_result
|
|
697
|
+
# Normalize HTTP response to match FastMCP format
|
|
698
|
+
normalized_result = self._normalize_http_result(result)
|
|
699
|
+
self.logger.info(f"✅ HTTP fallback successful in {duration_ms}ms")
|
|
700
|
+
return normalized_result
|
|
709
701
|
|
|
710
702
|
except ImportError:
|
|
711
703
|
raise RuntimeError("httpx not available for HTTP fallback")
|
|
@@ -215,11 +215,6 @@ class APIDependencyResolutionStep(PipelineStep):
|
|
|
215
215
|
|
|
216
216
|
# Import here to avoid circular imports
|
|
217
217
|
from ...engine.dependency_injector import get_global_injector
|
|
218
|
-
from ...engine.full_mcp_proxy import EnhancedFullMCPProxy, FullMCPProxy
|
|
219
|
-
from ...engine.mcp_client_proxy import (
|
|
220
|
-
EnhancedMCPClientProxy,
|
|
221
|
-
MCPClientProxy,
|
|
222
|
-
)
|
|
223
218
|
|
|
224
219
|
injector = get_global_injector()
|
|
225
220
|
|
|
@@ -421,87 +416,3 @@ class APIDependencyResolutionStep(PipelineStep):
|
|
|
421
416
|
f"❌ Failed to process API heartbeat response for rewiring: {e}"
|
|
422
417
|
)
|
|
423
418
|
# Don't raise - this should not break the heartbeat loop
|
|
424
|
-
|
|
425
|
-
def _determine_api_proxy_type_for_capability(
|
|
426
|
-
self, capability: str, injector
|
|
427
|
-
) -> str:
|
|
428
|
-
"""
|
|
429
|
-
Determine which proxy type to use for API route handlers.
|
|
430
|
-
|
|
431
|
-
Since McpAgent has been removed, all API route handlers now use MCPClientProxy
|
|
432
|
-
for McpMeshAgent parameters.
|
|
433
|
-
|
|
434
|
-
Args:
|
|
435
|
-
capability: The capability name to check
|
|
436
|
-
injector: The dependency injector instance
|
|
437
|
-
|
|
438
|
-
Returns:
|
|
439
|
-
"MCPClientProxy"
|
|
440
|
-
"""
|
|
441
|
-
# Note: This method always returns "MCPClientProxy" since McpAgent was removed.
|
|
442
|
-
# All McpMeshAgent parameters use MCPClientProxy.
|
|
443
|
-
self.logger.debug(
|
|
444
|
-
f"🔍 API route handlers for capability '{capability}' → using MCPClientProxy"
|
|
445
|
-
)
|
|
446
|
-
return "MCPClientProxy"
|
|
447
|
-
|
|
448
|
-
def _create_proxy_for_api(
|
|
449
|
-
self,
|
|
450
|
-
proxy_type: str,
|
|
451
|
-
endpoint: str,
|
|
452
|
-
dep_function_name: str,
|
|
453
|
-
kwargs_config: dict,
|
|
454
|
-
):
|
|
455
|
-
"""
|
|
456
|
-
Create the appropriate proxy instance for API route handlers.
|
|
457
|
-
|
|
458
|
-
Args:
|
|
459
|
-
proxy_type: "FullMCPProxy" or "MCPClientProxy"
|
|
460
|
-
endpoint: Target endpoint URL
|
|
461
|
-
dep_function_name: Target function name
|
|
462
|
-
kwargs_config: Additional configuration (timeout, retry, etc.)
|
|
463
|
-
|
|
464
|
-
Returns:
|
|
465
|
-
Proxy instance
|
|
466
|
-
"""
|
|
467
|
-
from ...engine.full_mcp_proxy import EnhancedFullMCPProxy, FullMCPProxy
|
|
468
|
-
from ...engine.mcp_client_proxy import EnhancedMCPClientProxy, MCPClientProxy
|
|
469
|
-
|
|
470
|
-
if proxy_type == "FullMCPProxy":
|
|
471
|
-
# Use enhanced proxy if kwargs available
|
|
472
|
-
if kwargs_config:
|
|
473
|
-
proxy = EnhancedFullMCPProxy(
|
|
474
|
-
endpoint,
|
|
475
|
-
dep_function_name,
|
|
476
|
-
kwargs_config=kwargs_config,
|
|
477
|
-
)
|
|
478
|
-
self.logger.debug(
|
|
479
|
-
f"🔧 Created EnhancedFullMCPProxy for API with kwargs: {kwargs_config}"
|
|
480
|
-
)
|
|
481
|
-
else:
|
|
482
|
-
proxy = FullMCPProxy(
|
|
483
|
-
endpoint,
|
|
484
|
-
dep_function_name,
|
|
485
|
-
kwargs_config=kwargs_config,
|
|
486
|
-
)
|
|
487
|
-
self.logger.debug("🔧 Created FullMCPProxy for API (no kwargs)")
|
|
488
|
-
return proxy
|
|
489
|
-
else:
|
|
490
|
-
# Use enhanced proxy if kwargs available
|
|
491
|
-
if kwargs_config:
|
|
492
|
-
proxy = EnhancedMCPClientProxy(
|
|
493
|
-
endpoint,
|
|
494
|
-
dep_function_name,
|
|
495
|
-
kwargs_config=kwargs_config,
|
|
496
|
-
)
|
|
497
|
-
self.logger.debug(
|
|
498
|
-
f"🔧 Created EnhancedMCPClientProxy for API with kwargs: {kwargs_config}"
|
|
499
|
-
)
|
|
500
|
-
else:
|
|
501
|
-
proxy = MCPClientProxy(
|
|
502
|
-
endpoint,
|
|
503
|
-
dep_function_name,
|
|
504
|
-
kwargs_config=kwargs_config,
|
|
505
|
-
)
|
|
506
|
-
self.logger.debug("🔧 Created MCPClientProxy for API (no kwargs)")
|
|
507
|
-
return proxy
|
|
@@ -42,7 +42,7 @@ class APIFastHeartbeatStep(PipelineStep):
|
|
|
42
42
|
Returns:
|
|
43
43
|
PipelineResult with fast_heartbeat_status in context
|
|
44
44
|
"""
|
|
45
|
-
self.logger.
|
|
45
|
+
self.logger.trace("Starting API fast heartbeat check...")
|
|
46
46
|
|
|
47
47
|
result = PipelineResult(message="API fast heartbeat check completed")
|
|
48
48
|
|
|
@@ -57,7 +57,7 @@ class APIFastHeartbeatStep(PipelineStep):
|
|
|
57
57
|
if not registry_wrapper:
|
|
58
58
|
raise ValueError("registry_wrapper is required in context")
|
|
59
59
|
|
|
60
|
-
self.logger.
|
|
60
|
+
self.logger.trace(
|
|
61
61
|
f"🚀 Performing API fast heartbeat check for service '{service_id}'"
|
|
62
62
|
)
|
|
63
63
|
|
|
@@ -114,4 +114,4 @@ class APIFastHeartbeatStep(PipelineStep):
|
|
|
114
114
|
if key not in result.context:
|
|
115
115
|
result.add_context(key, value)
|
|
116
116
|
|
|
117
|
-
return result
|
|
117
|
+
return result
|