mcp-mesh 0.7.12__py3-none-any.whl → 0.7.14__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 (41) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/__init__.py +1 -22
  3. _mcp_mesh/engine/async_mcp_client.py +88 -25
  4. _mcp_mesh/engine/decorator_registry.py +10 -9
  5. _mcp_mesh/engine/dependency_injector.py +64 -53
  6. _mcp_mesh/engine/mesh_llm_agent.py +119 -5
  7. _mcp_mesh/engine/mesh_llm_agent_injector.py +30 -0
  8. _mcp_mesh/engine/session_aware_client.py +3 -3
  9. _mcp_mesh/engine/unified_mcp_proxy.py +82 -90
  10. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -89
  11. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +3 -3
  12. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +30 -28
  13. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +16 -18
  14. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +5 -5
  15. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +3 -3
  16. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +6 -6
  17. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +1 -1
  18. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +15 -11
  19. _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +3 -3
  20. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +37 -268
  21. _mcp_mesh/pipeline/mcp_startup/lifespan_factory.py +142 -0
  22. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +57 -93
  23. _mcp_mesh/pipeline/shared/registry_connection.py +1 -1
  24. _mcp_mesh/shared/health_check_manager.py +313 -0
  25. _mcp_mesh/shared/logging_config.py +190 -7
  26. _mcp_mesh/shared/registry_client_wrapper.py +8 -8
  27. _mcp_mesh/shared/sse_parser.py +19 -17
  28. _mcp_mesh/tracing/execution_tracer.py +26 -1
  29. _mcp_mesh/tracing/fastapi_tracing_middleware.py +3 -4
  30. _mcp_mesh/tracing/trace_context_helper.py +25 -6
  31. {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.14.dist-info}/METADATA +1 -1
  32. {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.14.dist-info}/RECORD +38 -39
  33. mesh/__init__.py +3 -1
  34. mesh/decorators.py +81 -43
  35. mesh/helpers.py +72 -4
  36. mesh/types.py +48 -4
  37. _mcp_mesh/engine/full_mcp_proxy.py +0 -641
  38. _mcp_mesh/engine/mcp_client_proxy.py +0 -457
  39. _mcp_mesh/shared/health_check_cache.py +0 -246
  40. {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.14.dist-info}/WHEEL +0 -0
  41. {mcp_mesh-0.7.12.dist-info → mcp_mesh-0.7.14.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
- {**kwargs, "output_mode": self.output_mode}
675
+ {**effective_kwargs, "output_mode": self.output_mode}
589
676
  if self.output_mode
590
- else kwargs
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), model/api_key (provider has them),
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
- return self._parse_response(assistant_message.content)
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
- client = MCPClientProxy(target_agent["endpoint"], capability)
75
+ proxy = UnifiedMCPProxy(target_agent["endpoint"], capability)
76
76
 
77
77
  try:
78
- result = await client._async_call(**arguments)
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 replaces both MCPClientProxy and FullMCPProxy with a single implementation
4
- that uses FastMCP's superior client capabilities.
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
- # Add telemetry metadata to converted result if enabled
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
- data = None
618
-
619
- # Handle SSE format
620
- if response_text.startswith("event:") or "data:" in response_text:
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
- if isinstance(result, dict) and "content" in result:
677
- self.logger.info(
678
- f"✅ Successfully parsed result with content in {duration_ms}ms"
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.debug("Starting API fast heartbeat check...")
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.debug(
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