mcp-mesh 0.8.1__py3-none-any.whl → 0.9.0b2__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/mesh_llm_agent.py +17 -10
- _mcp_mesh/engine/mesh_llm_agent_injector.py +27 -10
- _mcp_mesh/engine/provider_handlers/__init__.py +2 -0
- _mcp_mesh/engine/provider_handlers/base_provider_handler.py +177 -0
- _mcp_mesh/engine/provider_handlers/claude_handler.py +129 -102
- _mcp_mesh/engine/provider_handlers/gemini_handler.py +218 -48
- _mcp_mesh/engine/provider_handlers/generic_handler.py +31 -0
- _mcp_mesh/engine/provider_handlers/openai_handler.py +4 -1
- _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +18 -0
- _mcp_mesh/tracing/trace_context_helper.py +5 -3
- {mcp_mesh-0.8.1.dist-info → mcp_mesh-0.9.0b2.dist-info}/METADATA +2 -2
- {mcp_mesh-0.8.1.dist-info → mcp_mesh-0.9.0b2.dist-info}/RECORD +16 -16
- mesh/helpers.py +39 -46
- {mcp_mesh-0.8.1.dist-info → mcp_mesh-0.9.0b2.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.8.1.dist-info → mcp_mesh-0.9.0b2.dist-info}/licenses/LICENSE +0 -0
_mcp_mesh/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ import json
|
|
|
9
9
|
import logging
|
|
10
10
|
import time
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import Any,
|
|
12
|
+
from typing import Any, Literal, Optional, Union
|
|
13
13
|
|
|
14
14
|
from pydantic import BaseModel
|
|
15
15
|
|
|
@@ -539,8 +539,8 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
539
539
|
|
|
540
540
|
logger.debug(
|
|
541
541
|
f"📥 Received response from mesh provider: "
|
|
542
|
-
f"content={message_dict.get('content'
|
|
543
|
-
f"tool_calls={len(message_dict.get('tool_calls'
|
|
542
|
+
f"content={(message_dict.get('content') or '')[:200]}..., "
|
|
543
|
+
f"tool_calls={len(message_dict.get('tool_calls') or [])}"
|
|
544
544
|
)
|
|
545
545
|
|
|
546
546
|
return MockResponse(message_dict)
|
|
@@ -618,13 +618,20 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
618
618
|
# Render base system prompt (from template or literal) with effective context
|
|
619
619
|
base_system_prompt = self._render_system_prompt(effective_context)
|
|
620
620
|
|
|
621
|
-
# Phase 2:
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
621
|
+
# Phase 2: Format system prompt
|
|
622
|
+
if self._is_mesh_delegated:
|
|
623
|
+
# Delegate path: Just use base prompt + basic tool instructions
|
|
624
|
+
# Provider will add vendor-specific formatting
|
|
625
|
+
system_content = base_system_prompt
|
|
626
|
+
if self._tool_schemas:
|
|
627
|
+
system_content += "\n\nYou have access to tools. Use them when needed to gather information."
|
|
628
|
+
else:
|
|
629
|
+
# Direct path: Use vendor handler for vendor-specific optimizations
|
|
630
|
+
system_content = self._provider_handler.format_system_prompt(
|
|
631
|
+
base_prompt=base_system_prompt,
|
|
632
|
+
tool_schemas=self._tool_schemas,
|
|
633
|
+
output_type=self.output_type,
|
|
634
|
+
)
|
|
628
635
|
|
|
629
636
|
# Debug: Log system prompt (truncated for privacy)
|
|
630
637
|
logger.debug(
|
|
@@ -123,11 +123,14 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
123
123
|
|
|
124
124
|
# Set factory for per-call context agent creation (template support)
|
|
125
125
|
if config.get("is_template", False):
|
|
126
|
+
|
|
126
127
|
def create_context_agent(
|
|
127
128
|
context_value: Any, _func_id: str = function_id
|
|
128
129
|
) -> MeshLlmAgent:
|
|
129
130
|
"""Factory to create MeshLlmAgent with context for template rendering."""
|
|
130
|
-
return self._create_llm_agent(
|
|
131
|
+
return self._create_llm_agent(
|
|
132
|
+
_func_id, context_value=context_value
|
|
133
|
+
)
|
|
131
134
|
|
|
132
135
|
wrapper._mesh_create_context_agent = create_context_agent
|
|
133
136
|
logger.info(
|
|
@@ -262,17 +265,25 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
262
265
|
filter_config = llm_metadata.config.get("filter")
|
|
263
266
|
has_filter = filter_config is not None and len(filter_config) > 0
|
|
264
267
|
|
|
268
|
+
# CRITICAL FIX: Always set config if not already set (prevents KeyError during race condition)
|
|
269
|
+
# When provider arrives before tools, config must be available for inject_llm_agent
|
|
270
|
+
# to create the LLM agent with context. Without this, _create_llm_agent fails with KeyError.
|
|
271
|
+
if "config" not in self._llm_agents[function_id] and llm_metadata:
|
|
272
|
+
self._llm_agents[function_id]["config"] = llm_metadata.config
|
|
273
|
+
self._llm_agents[function_id]["output_type"] = llm_metadata.output_type
|
|
274
|
+
self._llm_agents[function_id]["param_name"] = llm_metadata.param_name
|
|
275
|
+
self._llm_agents[function_id]["function"] = llm_metadata.function
|
|
276
|
+
logger.debug(
|
|
277
|
+
f"📋 Set config from DecoratorRegistry for '{function_id}' (awaiting tools)"
|
|
278
|
+
)
|
|
279
|
+
|
|
265
280
|
# If no filter specified, initialize empty tools data so we can create LLM agent without tools
|
|
266
281
|
# This supports simple LLM calls (text generation) that don't need tool calling
|
|
267
282
|
if not has_filter and "tools_metadata" not in self._llm_agents[function_id]:
|
|
268
283
|
self._llm_agents[function_id].update(
|
|
269
284
|
{
|
|
270
|
-
"config": llm_metadata.config if llm_metadata else {},
|
|
271
|
-
"output_type": llm_metadata.output_type if llm_metadata else None,
|
|
272
|
-
"param_name": llm_metadata.param_name if llm_metadata else "llm",
|
|
273
285
|
"tools_metadata": [], # No tools for simple LLM calls
|
|
274
286
|
"tools_proxies": {}, # No tool proxies needed
|
|
275
|
-
"function": llm_metadata.function if llm_metadata else None,
|
|
276
287
|
}
|
|
277
288
|
)
|
|
278
289
|
logger.info(
|
|
@@ -568,7 +579,9 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
568
579
|
)
|
|
569
580
|
|
|
570
581
|
# Check if templates are enabled
|
|
571
|
-
is_template =
|
|
582
|
+
is_template = (
|
|
583
|
+
config_dict.get("is_template", False) if config_dict else False
|
|
584
|
+
)
|
|
572
585
|
|
|
573
586
|
if is_template and config_dict:
|
|
574
587
|
# Templates enabled - create per-call agent with context
|
|
@@ -739,13 +752,17 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
739
752
|
)
|
|
740
753
|
|
|
741
754
|
# Create MeshLlmAgent with both metadata and proxies
|
|
755
|
+
# Use .get() with defaults for tools_metadata/proxies to handle race condition
|
|
756
|
+
# where provider arrives before tools (filter-based agents)
|
|
742
757
|
llm_agent = MeshLlmAgent(
|
|
743
758
|
config=llm_config,
|
|
744
|
-
filtered_tools=llm_agent_data
|
|
745
|
-
"tools_metadata"
|
|
746
|
-
|
|
759
|
+
filtered_tools=llm_agent_data.get(
|
|
760
|
+
"tools_metadata", []
|
|
761
|
+
), # Metadata for schema building (empty if tools not yet received)
|
|
747
762
|
output_type=llm_agent_data["output_type"],
|
|
748
|
-
tool_proxies=llm_agent_data
|
|
763
|
+
tool_proxies=llm_agent_data.get(
|
|
764
|
+
"tools_proxies", {}
|
|
765
|
+
), # Proxies for execution (empty if tools not yet received)
|
|
749
766
|
template_path=template_path if is_template else None,
|
|
750
767
|
context_value=context_value if is_template else None,
|
|
751
768
|
provider_proxy=llm_agent_data.get(
|
|
@@ -9,6 +9,7 @@ from .base_provider_handler import (
|
|
|
9
9
|
BASE_TOOL_INSTRUCTIONS,
|
|
10
10
|
CLAUDE_ANTI_XML_INSTRUCTION,
|
|
11
11
|
BaseProviderHandler,
|
|
12
|
+
is_simple_schema,
|
|
12
13
|
make_schema_strict,
|
|
13
14
|
)
|
|
14
15
|
from .claude_handler import ClaudeHandler
|
|
@@ -22,6 +23,7 @@ __all__ = [
|
|
|
22
23
|
"BASE_TOOL_INSTRUCTIONS",
|
|
23
24
|
"CLAUDE_ANTI_XML_INSTRUCTION",
|
|
24
25
|
# Utilities
|
|
26
|
+
"is_simple_schema",
|
|
25
27
|
"make_schema_strict",
|
|
26
28
|
# Handlers
|
|
27
29
|
"BaseProviderHandler",
|
|
@@ -6,9 +6,12 @@ that customize how different LLM vendors (Claude, OpenAI, Gemini, etc.) are call
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import copy
|
|
9
|
+
import logging
|
|
9
10
|
from abc import ABC, abstractmethod
|
|
10
11
|
from typing import Any, Optional
|
|
11
12
|
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
12
15
|
from pydantic import BaseModel
|
|
13
16
|
|
|
14
17
|
# ============================================================================
|
|
@@ -61,6 +64,144 @@ def make_schema_strict(
|
|
|
61
64
|
return result
|
|
62
65
|
|
|
63
66
|
|
|
67
|
+
def is_simple_schema(schema: dict[str, Any]) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Check if a JSON schema is simple enough for hint mode.
|
|
70
|
+
|
|
71
|
+
Simple schema criteria:
|
|
72
|
+
- Less than 5 fields
|
|
73
|
+
- All fields are basic types (str, int, float, bool, list)
|
|
74
|
+
- No nested Pydantic models ($ref or nested objects with properties)
|
|
75
|
+
|
|
76
|
+
This is used by provider handlers to decide between hint mode
|
|
77
|
+
(prompt-based JSON instructions) and strict mode (response_format).
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
schema: JSON schema dict
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if schema is simple, False otherwise
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
properties = schema.get("properties", {})
|
|
87
|
+
|
|
88
|
+
# Check field count
|
|
89
|
+
if len(properties) >= 5:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
# Check for nested objects or complex types
|
|
93
|
+
for field_name, field_schema in properties.items():
|
|
94
|
+
field_type = field_schema.get("type")
|
|
95
|
+
|
|
96
|
+
# Check for nested objects (indicates nested Pydantic model)
|
|
97
|
+
if field_type == "object" and "properties" in field_schema:
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# Check for $ref (nested model reference)
|
|
101
|
+
if "$ref" in field_schema:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
# Check array items for complex types
|
|
105
|
+
if field_type == "array":
|
|
106
|
+
items = field_schema.get("items", {})
|
|
107
|
+
if items.get("type") == "object" or "$ref" in items:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
return True
|
|
111
|
+
except Exception:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def sanitize_schema_for_structured_output(schema: dict[str, Any]) -> dict[str, Any]:
|
|
116
|
+
"""
|
|
117
|
+
Sanitize a JSON schema by removing validation keywords unsupported by LLM APIs.
|
|
118
|
+
|
|
119
|
+
LLM structured output APIs (Claude, OpenAI, Gemini) typically only support
|
|
120
|
+
the structural parts of JSON Schema, not validation constraints. This function
|
|
121
|
+
removes unsupported keywords to ensure uniform behavior across all providers.
|
|
122
|
+
|
|
123
|
+
Removed keywords:
|
|
124
|
+
- minimum, maximum (number range)
|
|
125
|
+
- exclusiveMinimum, exclusiveMaximum (exclusive bounds)
|
|
126
|
+
- minLength, maxLength (string length)
|
|
127
|
+
- minItems, maxItems (array size)
|
|
128
|
+
- pattern (regex validation)
|
|
129
|
+
- multipleOf (divisibility)
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
schema: JSON schema dict (will not be mutated)
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
New schema with unsupported validation keywords removed
|
|
136
|
+
"""
|
|
137
|
+
result = copy.deepcopy(schema)
|
|
138
|
+
_strip_unsupported_keywords_recursive(result)
|
|
139
|
+
logger.debug(
|
|
140
|
+
"Sanitized schema for structured output (removed validation-only keywords)"
|
|
141
|
+
)
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Keywords that are validation-only and not supported by LLM structured output APIs
|
|
146
|
+
_UNSUPPORTED_SCHEMA_KEYWORDS = {
|
|
147
|
+
"minimum",
|
|
148
|
+
"maximum",
|
|
149
|
+
"exclusiveMinimum",
|
|
150
|
+
"exclusiveMaximum",
|
|
151
|
+
"minLength",
|
|
152
|
+
"maxLength",
|
|
153
|
+
"minItems",
|
|
154
|
+
"maxItems",
|
|
155
|
+
"pattern",
|
|
156
|
+
"multipleOf",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _strip_unsupported_keywords_recursive(obj: Any) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Recursively strip unsupported validation keywords from a schema object.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
obj: Schema object to process (mutated in place)
|
|
166
|
+
"""
|
|
167
|
+
if not isinstance(obj, dict):
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
# Remove unsupported keywords at this level
|
|
171
|
+
for keyword in _UNSUPPORTED_SCHEMA_KEYWORDS:
|
|
172
|
+
obj.pop(keyword, None)
|
|
173
|
+
|
|
174
|
+
# Process $defs (Pydantic uses this for nested models)
|
|
175
|
+
if "$defs" in obj:
|
|
176
|
+
for def_schema in obj["$defs"].values():
|
|
177
|
+
_strip_unsupported_keywords_recursive(def_schema)
|
|
178
|
+
|
|
179
|
+
# Process properties
|
|
180
|
+
if "properties" in obj:
|
|
181
|
+
for prop_schema in obj["properties"].values():
|
|
182
|
+
_strip_unsupported_keywords_recursive(prop_schema)
|
|
183
|
+
|
|
184
|
+
# Process items (for arrays)
|
|
185
|
+
if "items" in obj:
|
|
186
|
+
items = obj["items"]
|
|
187
|
+
if isinstance(items, dict):
|
|
188
|
+
_strip_unsupported_keywords_recursive(items)
|
|
189
|
+
elif isinstance(items, list):
|
|
190
|
+
for item in items:
|
|
191
|
+
_strip_unsupported_keywords_recursive(item)
|
|
192
|
+
|
|
193
|
+
# Process prefixItems (tuple validation)
|
|
194
|
+
if "prefixItems" in obj:
|
|
195
|
+
for item in obj["prefixItems"]:
|
|
196
|
+
_strip_unsupported_keywords_recursive(item)
|
|
197
|
+
|
|
198
|
+
# Process anyOf, oneOf, allOf
|
|
199
|
+
for key in ("anyOf", "oneOf", "allOf"):
|
|
200
|
+
if key in obj:
|
|
201
|
+
for item in obj[key]:
|
|
202
|
+
_strip_unsupported_keywords_recursive(item)
|
|
203
|
+
|
|
204
|
+
|
|
64
205
|
def _add_strict_constraints_recursive(obj: Any, add_all_required: bool) -> None:
|
|
65
206
|
"""
|
|
66
207
|
Recursively add strict constraints to a schema object.
|
|
@@ -223,6 +364,42 @@ class BaseProviderHandler(ABC):
|
|
|
223
364
|
"json_mode": False,
|
|
224
365
|
}
|
|
225
366
|
|
|
367
|
+
def apply_structured_output(
|
|
368
|
+
self,
|
|
369
|
+
output_schema: dict[str, Any],
|
|
370
|
+
output_type_name: Optional[str],
|
|
371
|
+
model_params: dict[str, Any],
|
|
372
|
+
) -> dict[str, Any]:
|
|
373
|
+
"""
|
|
374
|
+
Apply vendor-specific structured output handling to model params.
|
|
375
|
+
|
|
376
|
+
This is used by LLM providers (via mesh) when they receive an output_schema
|
|
377
|
+
from a consumer. Each vendor can customize how structured output is enforced.
|
|
378
|
+
|
|
379
|
+
Default behavior: Apply response_format with strict schema.
|
|
380
|
+
Override in subclasses for vendor-specific behavior (e.g., Claude hint mode).
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
output_schema: JSON schema dict from consumer
|
|
384
|
+
output_type_name: Name of the output type (e.g., "AnalysisResult")
|
|
385
|
+
model_params: Current model parameters dict (will be modified)
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Modified model_params with structured output settings applied
|
|
389
|
+
"""
|
|
390
|
+
# Sanitize schema first to remove unsupported validation keywords
|
|
391
|
+
sanitized_schema = sanitize_schema_for_structured_output(output_schema)
|
|
392
|
+
strict_schema = make_schema_strict(sanitized_schema, add_all_required=True)
|
|
393
|
+
model_params["response_format"] = {
|
|
394
|
+
"type": "json_schema",
|
|
395
|
+
"json_schema": {
|
|
396
|
+
"name": output_type_name or "Response",
|
|
397
|
+
"schema": strict_schema,
|
|
398
|
+
"strict": True,
|
|
399
|
+
},
|
|
400
|
+
}
|
|
401
|
+
return model_params
|
|
402
|
+
|
|
226
403
|
def __repr__(self) -> str:
|
|
227
404
|
"""String representation of handler."""
|
|
228
405
|
return f"{self.__class__.__name__}(vendor='{self.vendor}')"
|