mcp-mesh 0.6.1__py3-none-any.whl → 0.6.3__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/llm_config.py +10 -1
- _mcp_mesh/engine/mesh_llm_agent.py +51 -33
- _mcp_mesh/engine/mesh_llm_agent_injector.py +72 -9
- _mcp_mesh/engine/provider_handlers/claude_handler.py +322 -42
- _mcp_mesh/engine/provider_handlers/openai_handler.py +65 -9
- _mcp_mesh/engine/response_parser.py +54 -15
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +54 -21
- _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +43 -26
- {mcp_mesh-0.6.1.dist-info → mcp_mesh-0.6.3.dist-info}/METADATA +1 -1
- {mcp_mesh-0.6.1.dist-info → mcp_mesh-0.6.3.dist-info}/RECORD +14 -14
- {mcp_mesh-0.6.1.dist-info → mcp_mesh-0.6.3.dist-info}/WHEEL +1 -1
- mesh/decorators.py +39 -2
- {mcp_mesh-0.6.1.dist-info → mcp_mesh-0.6.3.dist-info}/licenses/LICENSE +0 -0
_mcp_mesh/__init__.py
CHANGED
_mcp_mesh/engine/llm_config.py
CHANGED
|
@@ -17,7 +17,7 @@ class LLMConfig:
|
|
|
17
17
|
Supports both direct LiteLLM providers (string) and mesh delegation (dict).
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
provider: Union[str,
|
|
20
|
+
provider: Union[str, dict[str, Any]] = "claude"
|
|
21
21
|
"""LLM provider - string for direct LiteLLM (e.g., 'claude', 'openai') or dict for mesh delegation
|
|
22
22
|
Mesh delegation format: {"capability": "llm", "tags": ["claude"], "version": ">=1.0.0"}"""
|
|
23
23
|
|
|
@@ -33,6 +33,9 @@ class LLMConfig:
|
|
|
33
33
|
system_prompt: Optional[str] = None
|
|
34
34
|
"""Optional system prompt to prepend to all interactions"""
|
|
35
35
|
|
|
36
|
+
output_mode: Optional[str] = None
|
|
37
|
+
"""Output mode override: 'strict', 'hint', or 'text'. If None, auto-detected by handler."""
|
|
38
|
+
|
|
36
39
|
def __post_init__(self):
|
|
37
40
|
"""Validate configuration after initialization."""
|
|
38
41
|
if self.max_iterations < 1:
|
|
@@ -43,3 +46,9 @@ class LLMConfig:
|
|
|
43
46
|
# Only validate model for string providers (not needed for mesh delegation)
|
|
44
47
|
if isinstance(self.provider, str) and not self.model:
|
|
45
48
|
raise ValueError("model cannot be empty when using string provider")
|
|
49
|
+
|
|
50
|
+
# Validate output_mode if provided
|
|
51
|
+
if self.output_mode and self.output_mode not in ("strict", "hint", "text"):
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"output_mode must be 'strict', 'hint', or 'text', got '{self.output_mode}'"
|
|
54
|
+
)
|
|
@@ -58,7 +58,7 @@ class MeshLlmAgent:
|
|
|
58
58
|
self,
|
|
59
59
|
config: LLMConfig,
|
|
60
60
|
filtered_tools: list[dict[str, Any]],
|
|
61
|
-
output_type: type[BaseModel],
|
|
61
|
+
output_type: type[BaseModel] | type[str],
|
|
62
62
|
tool_proxies: Optional[dict[str, Any]] = None,
|
|
63
63
|
template_path: Optional[str] = None,
|
|
64
64
|
context_value: Optional[Any] = None,
|
|
@@ -71,7 +71,7 @@ class MeshLlmAgent:
|
|
|
71
71
|
Args:
|
|
72
72
|
config: LLM configuration (provider, model, api_key, etc.)
|
|
73
73
|
filtered_tools: List of tool metadata from registry (for schema building)
|
|
74
|
-
output_type: Pydantic BaseModel for response validation
|
|
74
|
+
output_type: Pydantic BaseModel for response validation, or str for plain text
|
|
75
75
|
tool_proxies: Optional map of function_name -> proxy for tool execution
|
|
76
76
|
template_path: Optional path to Jinja2 template file for system prompt
|
|
77
77
|
context_value: Optional context for template rendering (MeshContextModel, dict, or None)
|
|
@@ -87,6 +87,7 @@ class MeshLlmAgent:
|
|
|
87
87
|
self.max_iterations = config.max_iterations
|
|
88
88
|
self.output_type = output_type
|
|
89
89
|
self.system_prompt = config.system_prompt # Public attribute for tests
|
|
90
|
+
self.output_mode = config.output_mode # Output mode override (strict/hint/text)
|
|
90
91
|
self._iteration_count = 0
|
|
91
92
|
|
|
92
93
|
# Detect if using mesh delegation (provider is dict)
|
|
@@ -126,12 +127,19 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
126
127
|
- Once you have gathered all necessary information, provide your final response
|
|
127
128
|
"""
|
|
128
129
|
|
|
129
|
-
schema
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
# Only generate JSON schema for Pydantic models, not for str return type
|
|
131
|
+
if self.output_type is not str and hasattr(
|
|
132
|
+
self.output_type, "model_json_schema"
|
|
133
|
+
):
|
|
134
|
+
schema = self.output_type.model_json_schema()
|
|
135
|
+
schema_str = json.dumps(schema, indent=2)
|
|
136
|
+
self._cached_json_instructions = (
|
|
137
|
+
f"\n\nIMPORTANT: You must return your final response as valid JSON matching this schema:\n"
|
|
138
|
+
f"{schema_str}\n\nReturn ONLY the JSON object, no additional text."
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
# str return type - no JSON schema needed
|
|
142
|
+
self._cached_json_instructions = ""
|
|
135
143
|
|
|
136
144
|
logger.debug(
|
|
137
145
|
f"🤖 MeshLlmAgent initialized: provider={config.provider}, model={config.model}, "
|
|
@@ -483,22 +491,36 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
483
491
|
try:
|
|
484
492
|
# Call LLM (either direct LiteLLM or mesh-delegated)
|
|
485
493
|
try:
|
|
486
|
-
if
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
494
|
+
# Build kwargs with output_mode override if set
|
|
495
|
+
call_kwargs = (
|
|
496
|
+
{**kwargs, "output_mode": self.output_mode}
|
|
497
|
+
if self.output_mode
|
|
498
|
+
else kwargs
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Use provider handler to prepare vendor-specific request
|
|
502
|
+
request_params = self._provider_handler.prepare_request(
|
|
503
|
+
messages=messages,
|
|
504
|
+
tools=self._tool_schemas if self._tool_schemas else None,
|
|
505
|
+
output_type=self.output_type,
|
|
506
|
+
**call_kwargs,
|
|
507
|
+
)
|
|
495
508
|
|
|
496
|
-
|
|
497
|
-
#
|
|
509
|
+
if self._is_mesh_delegated:
|
|
510
|
+
# Mesh delegation: extract model_params to send to provider
|
|
511
|
+
# Exclude messages/tools (separate params), model/api_key (provider has them),
|
|
512
|
+
# and output_mode (only used locally by prepare_request)
|
|
498
513
|
model_params = {
|
|
499
514
|
k: v
|
|
500
515
|
for k, v in request_params.items()
|
|
501
|
-
if k
|
|
516
|
+
if k
|
|
517
|
+
not in [
|
|
518
|
+
"messages",
|
|
519
|
+
"tools",
|
|
520
|
+
"model",
|
|
521
|
+
"api_key",
|
|
522
|
+
"output_mode",
|
|
523
|
+
]
|
|
502
524
|
}
|
|
503
525
|
|
|
504
526
|
logger.debug(
|
|
@@ -509,19 +531,10 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
509
531
|
response = await self._call_mesh_provider(
|
|
510
532
|
messages=messages,
|
|
511
533
|
tools=self._tool_schemas if self._tool_schemas else None,
|
|
512
|
-
**model_params,
|
|
534
|
+
**model_params,
|
|
513
535
|
)
|
|
514
536
|
else:
|
|
515
|
-
# Direct LiteLLM call
|
|
516
|
-
# Phase 2: Use provider handler to prepare vendor-specific request
|
|
517
|
-
request_params = self._provider_handler.prepare_request(
|
|
518
|
-
messages=messages,
|
|
519
|
-
tools=self._tool_schemas if self._tool_schemas else None,
|
|
520
|
-
output_type=self.output_type,
|
|
521
|
-
**kwargs,
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
# Add model and API key (common to all vendors)
|
|
537
|
+
# Direct LiteLLM call: add model and API key
|
|
525
538
|
request_params["model"] = self.model
|
|
526
539
|
request_params["api_key"] = self.api_key
|
|
527
540
|
|
|
@@ -612,15 +625,20 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
612
625
|
"""
|
|
613
626
|
Parse LLM response into output type.
|
|
614
627
|
|
|
615
|
-
|
|
628
|
+
For str return type, returns content directly without parsing.
|
|
629
|
+
For Pydantic models, delegates to ResponseParser.
|
|
616
630
|
|
|
617
631
|
Args:
|
|
618
632
|
content: Response content from LLM
|
|
619
633
|
|
|
620
634
|
Returns:
|
|
621
|
-
|
|
635
|
+
Raw string (if output_type is str) or parsed Pydantic model instance
|
|
622
636
|
|
|
623
637
|
Raises:
|
|
624
638
|
ResponseParseError: If response doesn't match output_type schema or invalid JSON
|
|
625
639
|
"""
|
|
640
|
+
# For str return type, return content directly without parsing
|
|
641
|
+
if self.output_type is str:
|
|
642
|
+
return content
|
|
643
|
+
|
|
626
644
|
return ResponseParser.parse(content, self.output_type)
|
|
@@ -19,6 +19,35 @@ from .unified_mcp_proxy import UnifiedMCPProxy
|
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def extract_vendor_from_model(model: str) -> str | None:
|
|
23
|
+
"""
|
|
24
|
+
Extract vendor name from LiteLLM model string.
|
|
25
|
+
|
|
26
|
+
LiteLLM uses vendor/model format (e.g., "anthropic/claude-sonnet-4-5").
|
|
27
|
+
This extracts the vendor for provider handler selection.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
model: LiteLLM model string
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Vendor name (e.g., "anthropic", "openai") or None if not extractable
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
"anthropic/claude-sonnet-4-5" -> "anthropic"
|
|
37
|
+
"openai/gpt-4o" -> "openai"
|
|
38
|
+
"gpt-4" -> None (no vendor prefix)
|
|
39
|
+
"""
|
|
40
|
+
if not model:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
if "/" in model:
|
|
44
|
+
vendor = model.split("/")[0].lower().strip()
|
|
45
|
+
logger.debug(f"🔍 Extracted vendor '{vendor}' from model '{model}'")
|
|
46
|
+
return vendor
|
|
47
|
+
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
22
51
|
class MeshLlmAgentInjector(BaseInjector):
|
|
23
52
|
"""
|
|
24
53
|
Manages dynamic injection of MeshLlmAgent proxies.
|
|
@@ -86,9 +115,7 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
86
115
|
exc_info=True,
|
|
87
116
|
)
|
|
88
117
|
|
|
89
|
-
def process_llm_providers(
|
|
90
|
-
self, llm_providers: dict[str, dict[str, Any]]
|
|
91
|
-
) -> None:
|
|
118
|
+
def process_llm_providers(self, llm_providers: dict[str, dict[str, Any]]) -> None:
|
|
92
119
|
"""
|
|
93
120
|
Process llm_providers from registry response (v0.6.1 mesh delegation).
|
|
94
121
|
|
|
@@ -181,9 +208,7 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
181
208
|
"""
|
|
182
209
|
function_name = provider_data.get("name")
|
|
183
210
|
if not function_name:
|
|
184
|
-
raise ValueError(
|
|
185
|
-
f"Provider missing required 'name' field: {provider_data}"
|
|
186
|
-
)
|
|
211
|
+
raise ValueError(f"Provider missing required 'name' field: {provider_data}")
|
|
187
212
|
|
|
188
213
|
endpoint = provider_data.get("endpoint")
|
|
189
214
|
if not endpoint:
|
|
@@ -282,6 +307,22 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
282
307
|
llm_agent = self._create_llm_agent(function_id)
|
|
283
308
|
wrapper._mesh_update_llm_agent(llm_agent)
|
|
284
309
|
logger.info(f"🔄 Updated wrapper with MeshLlmAgent for '{function_id}'")
|
|
310
|
+
|
|
311
|
+
# Set factory for per-call context agent creation (template support)
|
|
312
|
+
# This allows the decorator's wrapper to create new agents with context per-call
|
|
313
|
+
config_dict = llm_metadata.config
|
|
314
|
+
if config_dict.get("is_template", False):
|
|
315
|
+
# Capture function_id by value using default argument to avoid closure issues
|
|
316
|
+
def create_context_agent(
|
|
317
|
+
context_value: Any, _func_id: str = function_id
|
|
318
|
+
) -> MeshLlmAgent:
|
|
319
|
+
"""Factory to create MeshLlmAgent with context for template rendering."""
|
|
320
|
+
return self._create_llm_agent(_func_id, context_value=context_value)
|
|
321
|
+
|
|
322
|
+
wrapper._mesh_create_context_agent = create_context_agent
|
|
323
|
+
logger.info(
|
|
324
|
+
f"🎯 Set context agent factory for template-based function '{function_id}'"
|
|
325
|
+
)
|
|
285
326
|
elif wrapper:
|
|
286
327
|
logger.warning(
|
|
287
328
|
f"⚠️ Wrapper for '{function_id}' found but has no _mesh_update_llm_agent method"
|
|
@@ -487,12 +528,28 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
487
528
|
api_key=config_dict.get("api_key", ""), # Will use ENV if empty
|
|
488
529
|
max_iterations=config_dict.get("max_iterations", 10),
|
|
489
530
|
system_prompt=config_dict.get("system_prompt"),
|
|
531
|
+
output_mode=config_dict.get(
|
|
532
|
+
"output_mode"
|
|
533
|
+
), # Pass through output_mode from decorator
|
|
490
534
|
)
|
|
491
535
|
|
|
492
536
|
# Phase 4: Template support - extract template metadata
|
|
493
537
|
is_template = config_dict.get("is_template", False)
|
|
494
538
|
template_path = config_dict.get("template_path")
|
|
495
539
|
|
|
540
|
+
# Determine vendor for provider handler selection
|
|
541
|
+
# Priority: 1) From registry (mesh delegation), 2) From model name, 3) None
|
|
542
|
+
vendor = llm_agent_data.get("vendor")
|
|
543
|
+
if not vendor:
|
|
544
|
+
# For direct LiteLLM calls, extract vendor from model name
|
|
545
|
+
# e.g., "anthropic/claude-sonnet-4-5" -> "anthropic"
|
|
546
|
+
model = config_dict.get("model", "")
|
|
547
|
+
vendor = extract_vendor_from_model(model)
|
|
548
|
+
if vendor:
|
|
549
|
+
logger.info(
|
|
550
|
+
f"🔍 Extracted vendor '{vendor}' from model '{model}' for handler selection"
|
|
551
|
+
)
|
|
552
|
+
|
|
496
553
|
# Create MeshLlmAgent with both metadata and proxies
|
|
497
554
|
llm_agent = MeshLlmAgent(
|
|
498
555
|
config=llm_config,
|
|
@@ -503,14 +560,20 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
503
560
|
tool_proxies=llm_agent_data["tools_proxies"], # Proxies for execution
|
|
504
561
|
template_path=template_path if is_template else None,
|
|
505
562
|
context_value=context_value if is_template else None,
|
|
506
|
-
provider_proxy=llm_agent_data.get(
|
|
507
|
-
|
|
563
|
+
provider_proxy=llm_agent_data.get(
|
|
564
|
+
"provider_proxy"
|
|
565
|
+
), # Provider proxy for mesh delegation
|
|
566
|
+
vendor=vendor, # Vendor for provider handler selection (from registry or model name)
|
|
508
567
|
)
|
|
509
568
|
|
|
510
569
|
logger.debug(
|
|
511
570
|
f"🤖 Created MeshLlmAgent for {function_id} with {len(llm_agent_data['tools_metadata'])} tools"
|
|
512
571
|
+ (f", template={template_path}" if is_template else "")
|
|
513
|
-
+ (
|
|
572
|
+
+ (
|
|
573
|
+
f", provider_proxy={llm_agent_data.get('provider_proxy').function_name if llm_agent_data.get('provider_proxy') else 'None'}"
|
|
574
|
+
if isinstance(config_dict.get("provider"), dict)
|
|
575
|
+
else ""
|
|
576
|
+
)
|
|
514
577
|
)
|
|
515
578
|
|
|
516
579
|
return llm_agent
|