mcp-mesh 0.8.0__py3-none-any.whl → 0.8.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/dependency_injector.py +9 -9
- _mcp_mesh/engine/mesh_llm_agent.py +14 -36
- _mcp_mesh/engine/mesh_llm_agent_injector.py +43 -78
- _mcp_mesh/engine/signature_analyzer.py +68 -58
- _mcp_mesh/engine/unified_mcp_proxy.py +1 -1
- _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +1 -5
- _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +6 -21
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +13 -37
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +9 -9
- _mcp_mesh/utils/fastmcp_schema_extractor.py +3 -3
- {mcp_mesh-0.8.0.dist-info → mcp_mesh-0.8.0b2.dist-info}/METADATA +6 -7
- {mcp_mesh-0.8.0.dist-info → mcp_mesh-0.8.0b2.dist-info}/RECORD +19 -19
- mesh/__init__.py +2 -12
- mesh/decorators.py +51 -182
- mesh/helpers.py +0 -52
- mesh/types.py +13 -40
- {mcp_mesh-0.8.0.dist-info → mcp_mesh-0.8.0b2.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.8.0.dist-info → mcp_mesh-0.8.0b2.dist-info}/licenses/LICENSE +0 -0
mesh/decorators.py
CHANGED
|
@@ -26,25 +26,6 @@ _runtime_processor: Any | None = None
|
|
|
26
26
|
_SHARED_AGENT_ID: str | None = None
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def _find_available_port() -> int:
|
|
30
|
-
"""
|
|
31
|
-
Find an available port by binding to port 0 and getting the OS-assigned port.
|
|
32
|
-
|
|
33
|
-
This is used when http_port=0 is specified to auto-assign a port.
|
|
34
|
-
Works reliably on all platforms (macOS, Linux, Windows) without external tools.
|
|
35
|
-
|
|
36
|
-
Returns:
|
|
37
|
-
int: An available port number
|
|
38
|
-
"""
|
|
39
|
-
import socket
|
|
40
|
-
|
|
41
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
42
|
-
s.bind(("127.0.0.1", 0))
|
|
43
|
-
s.listen(1)
|
|
44
|
-
port = s.getsockname()[1]
|
|
45
|
-
return port
|
|
46
|
-
|
|
47
|
-
|
|
48
29
|
def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
49
30
|
"""
|
|
50
31
|
Start basic uvicorn server immediately to prevent Python interpreter shutdown.
|
|
@@ -95,75 +76,8 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
95
76
|
app = FastAPI(title="MCP Mesh Agent (Starting)")
|
|
96
77
|
logger.debug("📦 IMMEDIATE UVICORN: Created minimal FastAPI app")
|
|
97
78
|
|
|
98
|
-
# Add
|
|
99
|
-
# This must be done
|
|
100
|
-
# regardless of agent's tracing configuration
|
|
101
|
-
try:
|
|
102
|
-
import json as json_module
|
|
103
|
-
|
|
104
|
-
class TraceArgumentStripperMiddleware:
|
|
105
|
-
"""Pure ASGI middleware to strip trace arguments from tool calls.
|
|
106
|
-
|
|
107
|
-
This middleware ALWAYS runs to strip _trace_id and _parent_span from
|
|
108
|
-
MCP tool arguments, preventing Pydantic validation errors when
|
|
109
|
-
meshctl --trace is used with agents that don't have tracing enabled.
|
|
110
|
-
"""
|
|
111
|
-
|
|
112
|
-
def __init__(self, app):
|
|
113
|
-
self.app = app
|
|
114
|
-
|
|
115
|
-
async def __call__(self, scope, receive, send):
|
|
116
|
-
if scope["type"] != "http":
|
|
117
|
-
await self.app(scope, receive, send)
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
async def receive_with_trace_stripping():
|
|
121
|
-
message = await receive()
|
|
122
|
-
if message["type"] == "http.request":
|
|
123
|
-
body = message.get("body", b"")
|
|
124
|
-
if body:
|
|
125
|
-
try:
|
|
126
|
-
payload = json_module.loads(body.decode("utf-8"))
|
|
127
|
-
if payload.get("method") == "tools/call":
|
|
128
|
-
arguments = payload.get("params", {}).get(
|
|
129
|
-
"arguments", {}
|
|
130
|
-
)
|
|
131
|
-
# Strip trace context fields from arguments
|
|
132
|
-
if (
|
|
133
|
-
"_trace_id" in arguments
|
|
134
|
-
or "_parent_span" in arguments
|
|
135
|
-
):
|
|
136
|
-
arguments.pop("_trace_id", None)
|
|
137
|
-
arguments.pop("_parent_span", None)
|
|
138
|
-
modified_body = json_module.dumps(
|
|
139
|
-
payload
|
|
140
|
-
).encode("utf-8")
|
|
141
|
-
logger.debug(
|
|
142
|
-
"[TRACE] Stripped trace fields from arguments"
|
|
143
|
-
)
|
|
144
|
-
return {
|
|
145
|
-
**message,
|
|
146
|
-
"body": modified_body,
|
|
147
|
-
}
|
|
148
|
-
except Exception as e:
|
|
149
|
-
logger.debug(
|
|
150
|
-
f"[TRACE] Failed to process body for stripping: {e}"
|
|
151
|
-
)
|
|
152
|
-
return message
|
|
153
|
-
|
|
154
|
-
await self.app(scope, receive_with_trace_stripping, send)
|
|
155
|
-
|
|
156
|
-
app.add_middleware(TraceArgumentStripperMiddleware)
|
|
157
|
-
logger.debug(
|
|
158
|
-
"📦 IMMEDIATE UVICORN: Added trace argument stripper middleware"
|
|
159
|
-
)
|
|
160
|
-
except Exception as e:
|
|
161
|
-
logger.warning(
|
|
162
|
-
f"⚠️ IMMEDIATE UVICORN: Failed to add trace argument stripper middleware: {e}"
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
# Add trace context middleware for distributed tracing (optional)
|
|
166
|
-
# This handles trace propagation and header injection when tracing is enabled
|
|
79
|
+
# Add trace context middleware for distributed tracing BEFORE app starts
|
|
80
|
+
# This must be done before uvicorn.run() since middleware can't be added after start
|
|
167
81
|
try:
|
|
168
82
|
import os
|
|
169
83
|
|
|
@@ -238,11 +152,11 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
238
152
|
except Exception as e:
|
|
239
153
|
logger.warning(f"Failed to set trace context: {e}")
|
|
240
154
|
|
|
241
|
-
# Create receive wrapper to
|
|
242
|
-
#
|
|
155
|
+
# Create body-modifying receive wrapper to strip trace fields from arguments
|
|
156
|
+
# This handles the case where trace context is passed in JSON-RPC arguments
|
|
243
157
|
import json as json_module
|
|
244
158
|
|
|
245
|
-
async def
|
|
159
|
+
async def receive_with_trace_stripping():
|
|
246
160
|
message = await receive()
|
|
247
161
|
if message["type"] == "http.request":
|
|
248
162
|
body = message.get("body", b"")
|
|
@@ -293,14 +207,29 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
293
207
|
parent_span = (
|
|
294
208
|
current_trace.parent_span
|
|
295
209
|
)
|
|
296
|
-
logger.debug(
|
|
297
|
-
f"[TRACE] Extracted trace context from arguments: trace_id={arg_trace_id}"
|
|
298
|
-
)
|
|
299
210
|
except Exception:
|
|
300
211
|
pass
|
|
212
|
+
|
|
213
|
+
# Strip trace context fields from arguments
|
|
214
|
+
if (
|
|
215
|
+
"_trace_id" in arguments
|
|
216
|
+
or "_parent_span" in arguments
|
|
217
|
+
):
|
|
218
|
+
arguments.pop("_trace_id", None)
|
|
219
|
+
arguments.pop("_parent_span", None)
|
|
220
|
+
modified_body = json_module.dumps(
|
|
221
|
+
payload
|
|
222
|
+
).encode("utf-8")
|
|
223
|
+
logger.debug(
|
|
224
|
+
"[TRACE] Stripped trace fields from arguments"
|
|
225
|
+
)
|
|
226
|
+
return {
|
|
227
|
+
**message,
|
|
228
|
+
"body": modified_body,
|
|
229
|
+
}
|
|
301
230
|
except Exception as e:
|
|
302
231
|
logger.debug(
|
|
303
|
-
f"[TRACE] Failed to process body
|
|
232
|
+
f"[TRACE] Failed to process body: {e}"
|
|
304
233
|
)
|
|
305
234
|
return message
|
|
306
235
|
|
|
@@ -320,7 +249,7 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
320
249
|
await send(message)
|
|
321
250
|
|
|
322
251
|
await self.app(
|
|
323
|
-
scope,
|
|
252
|
+
scope, receive_with_trace_stripping, send_with_trace_headers
|
|
324
253
|
)
|
|
325
254
|
|
|
326
255
|
app.add_middleware(TraceContextMiddleware)
|
|
@@ -370,19 +299,8 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
370
299
|
|
|
371
300
|
logger.debug("📦 IMMEDIATE UVICORN: Added status endpoints")
|
|
372
301
|
|
|
373
|
-
#
|
|
374
|
-
|
|
375
|
-
# - http_port>0 means use that specific port
|
|
376
|
-
# Note: The default is 8080 only if http_port was never specified,
|
|
377
|
-
# which is handled upstream in the @mesh.agent decorator
|
|
378
|
-
port = http_port
|
|
379
|
-
|
|
380
|
-
# Handle http_port=0: find an available port BEFORE starting uvicorn
|
|
381
|
-
# This is more reliable than detecting the port after uvicorn starts
|
|
382
|
-
# and works on all platforms (Linux containers don't have lsof installed)
|
|
383
|
-
if port == 0:
|
|
384
|
-
port = _find_available_port()
|
|
385
|
-
logger.info(f"🎯 IMMEDIATE UVICORN: Auto-assigned port {port} for agent")
|
|
302
|
+
# Determine port (0 means auto-assign)
|
|
303
|
+
port = http_port if http_port > 0 else 8080
|
|
386
304
|
|
|
387
305
|
logger.debug(
|
|
388
306
|
f"🚀 IMMEDIATE UVICORN: Starting uvicorn server on {http_host}:{port}"
|
|
@@ -618,25 +536,12 @@ def tool(
|
|
|
618
536
|
raise ValueError("dependency capability must be a string")
|
|
619
537
|
|
|
620
538
|
# Validate optional dependency fields
|
|
621
|
-
# Tags can be strings or arrays of strings (OR alternatives)
|
|
622
|
-
# e.g., ["required", ["python", "typescript"]] = required AND (python OR typescript)
|
|
623
539
|
dep_tags = dep.get("tags", [])
|
|
624
540
|
if not isinstance(dep_tags, list):
|
|
625
541
|
raise ValueError("dependency tags must be a list")
|
|
626
542
|
for tag in dep_tags:
|
|
627
|
-
if isinstance(tag, str):
|
|
628
|
-
|
|
629
|
-
elif isinstance(tag, list):
|
|
630
|
-
# OR alternative - validate inner tags are all strings
|
|
631
|
-
for inner_tag in tag:
|
|
632
|
-
if not isinstance(inner_tag, str):
|
|
633
|
-
raise ValueError(
|
|
634
|
-
"OR alternative tags must be strings"
|
|
635
|
-
)
|
|
636
|
-
else:
|
|
637
|
-
raise ValueError(
|
|
638
|
-
"tags must be strings or arrays of strings (OR alternatives)"
|
|
639
|
-
)
|
|
543
|
+
if not isinstance(tag, str):
|
|
544
|
+
raise ValueError("all dependency tags must be strings")
|
|
640
545
|
|
|
641
546
|
dep_version = dep.get("version")
|
|
642
547
|
if dep_version is not None and not isinstance(dep_version, str):
|
|
@@ -754,7 +659,7 @@ def agent(
|
|
|
754
659
|
http_port: int = 0,
|
|
755
660
|
enable_http: bool = True,
|
|
756
661
|
namespace: str = "default",
|
|
757
|
-
|
|
662
|
+
health_interval: int = 5, # Will be overridden by centralized defaults
|
|
758
663
|
health_check: Callable[[], Awaitable[Any]] | None = None,
|
|
759
664
|
health_check_ttl: int = 15,
|
|
760
665
|
auto_run: bool = True, # Changed to True by default!
|
|
@@ -779,7 +684,7 @@ def agent(
|
|
|
779
684
|
Environment variable: MCP_MESH_HTTP_ENABLED (takes precedence)
|
|
780
685
|
namespace: Agent namespace (default: "default")
|
|
781
686
|
Environment variable: MCP_MESH_NAMESPACE (takes precedence)
|
|
782
|
-
|
|
687
|
+
health_interval: Health check interval in seconds (default: 30)
|
|
783
688
|
Environment variable: MCP_MESH_HEALTH_INTERVAL (takes precedence)
|
|
784
689
|
health_check: Optional async function that returns HealthStatus
|
|
785
690
|
Called before heartbeat and on /health endpoint with TTL caching
|
|
@@ -796,7 +701,7 @@ def agent(
|
|
|
796
701
|
MCP_MESH_HTTP_PORT: Override http_port parameter (integer, 0-65535)
|
|
797
702
|
MCP_MESH_HTTP_ENABLED: Override enable_http parameter (boolean: true/false)
|
|
798
703
|
MCP_MESH_NAMESPACE: Override namespace parameter (string)
|
|
799
|
-
MCP_MESH_HEALTH_INTERVAL: Override
|
|
704
|
+
MCP_MESH_HEALTH_INTERVAL: Override health_interval parameter (integer, ≥1)
|
|
800
705
|
MCP_MESH_AUTO_RUN: Override auto_run parameter (boolean: true/false)
|
|
801
706
|
MCP_MESH_AUTO_RUN_INTERVAL: Override auto_run_interval parameter (integer, ≥1)
|
|
802
707
|
|
|
@@ -847,10 +752,10 @@ def agent(
|
|
|
847
752
|
if not isinstance(namespace, str):
|
|
848
753
|
raise ValueError("namespace must be a string")
|
|
849
754
|
|
|
850
|
-
if not isinstance(
|
|
851
|
-
raise ValueError("
|
|
852
|
-
if
|
|
853
|
-
raise ValueError("
|
|
755
|
+
if not isinstance(health_interval, int):
|
|
756
|
+
raise ValueError("health_interval must be an integer")
|
|
757
|
+
if health_interval < 1:
|
|
758
|
+
raise ValueError("health_interval must be at least 1 second")
|
|
854
759
|
|
|
855
760
|
if not isinstance(auto_run, bool):
|
|
856
761
|
raise ValueError("auto_run must be a boolean")
|
|
@@ -905,9 +810,9 @@ def agent(
|
|
|
905
810
|
# Import centralized defaults
|
|
906
811
|
from _mcp_mesh.shared.defaults import MeshDefaults
|
|
907
812
|
|
|
908
|
-
|
|
813
|
+
final_health_interval = get_config_value(
|
|
909
814
|
"MCP_MESH_HEALTH_INTERVAL",
|
|
910
|
-
override=
|
|
815
|
+
override=health_interval,
|
|
911
816
|
default=MeshDefaults.HEALTH_INTERVAL,
|
|
912
817
|
rule=ValidationRule.NONZERO_RULE,
|
|
913
818
|
)
|
|
@@ -938,7 +843,7 @@ def agent(
|
|
|
938
843
|
"http_port": final_http_port,
|
|
939
844
|
"enable_http": final_enable_http,
|
|
940
845
|
"namespace": final_namespace,
|
|
941
|
-
"
|
|
846
|
+
"health_interval": final_health_interval,
|
|
942
847
|
"health_check": health_check,
|
|
943
848
|
"health_check_ttl": health_check_ttl,
|
|
944
849
|
"auto_run": final_auto_run,
|
|
@@ -1077,10 +982,10 @@ def route(
|
|
|
1077
982
|
async def upload_resume(
|
|
1078
983
|
request: Request,
|
|
1079
984
|
file: UploadFile = File(...),
|
|
1080
|
-
|
|
1081
|
-
user_service: mesh.
|
|
985
|
+
pdf_agent: mesh.McpMeshAgent = None, # Injected by MCP Mesh
|
|
986
|
+
user_service: mesh.McpMeshAgent = None # Injected by MCP Mesh
|
|
1082
987
|
):
|
|
1083
|
-
result = await
|
|
988
|
+
result = await pdf_agent.extract_text_from_pdf(file)
|
|
1084
989
|
await user_service.update_profile(user_data, result)
|
|
1085
990
|
return {"success": True}
|
|
1086
991
|
"""
|
|
@@ -1109,25 +1014,12 @@ def route(
|
|
|
1109
1014
|
raise ValueError("dependency capability must be a string")
|
|
1110
1015
|
|
|
1111
1016
|
# Validate optional dependency fields
|
|
1112
|
-
# Tags can be strings or arrays of strings (OR alternatives)
|
|
1113
|
-
# e.g., ["required", ["python", "typescript"]] = required AND (python OR typescript)
|
|
1114
1017
|
dep_tags = dep.get("tags", [])
|
|
1115
1018
|
if not isinstance(dep_tags, list):
|
|
1116
1019
|
raise ValueError("dependency tags must be a list")
|
|
1117
1020
|
for tag in dep_tags:
|
|
1118
|
-
if isinstance(tag, str):
|
|
1119
|
-
|
|
1120
|
-
elif isinstance(tag, list):
|
|
1121
|
-
# OR alternative - validate inner tags are all strings
|
|
1122
|
-
for inner_tag in tag:
|
|
1123
|
-
if not isinstance(inner_tag, str):
|
|
1124
|
-
raise ValueError(
|
|
1125
|
-
"OR alternative tags must be strings"
|
|
1126
|
-
)
|
|
1127
|
-
else:
|
|
1128
|
-
raise ValueError(
|
|
1129
|
-
"tags must be strings or arrays of strings (OR alternatives)"
|
|
1130
|
-
)
|
|
1021
|
+
if not isinstance(tag, str):
|
|
1022
|
+
raise ValueError("all dependency tags must be strings")
|
|
1131
1023
|
|
|
1132
1024
|
dep_version = dep.get("version")
|
|
1133
1025
|
if dep_version is not None and not isinstance(dep_version, str):
|
|
@@ -1413,34 +1305,6 @@ def llm(
|
|
|
1413
1305
|
rule=ValidationRule.STRING_RULE,
|
|
1414
1306
|
)
|
|
1415
1307
|
|
|
1416
|
-
# Resolve model with env var override
|
|
1417
|
-
resolved_model = get_config_value(
|
|
1418
|
-
"MESH_LLM_MODEL",
|
|
1419
|
-
override=model,
|
|
1420
|
-
default=None,
|
|
1421
|
-
rule=ValidationRule.STRING_RULE,
|
|
1422
|
-
)
|
|
1423
|
-
|
|
1424
|
-
# Warn about missing configuration parameters
|
|
1425
|
-
if not system_prompt and not system_prompt_file:
|
|
1426
|
-
logger.warning(
|
|
1427
|
-
f"⚠️ @mesh.llm: No 'system_prompt' specified for function '{func.__name__}'. "
|
|
1428
|
-
f"Using default: 'You are a helpful assistant.' "
|
|
1429
|
-
f"Consider adding a custom system_prompt for better results."
|
|
1430
|
-
)
|
|
1431
|
-
|
|
1432
|
-
if isinstance(provider, str) and provider == "claude" and not resolved_model:
|
|
1433
|
-
logger.warning(
|
|
1434
|
-
f"⚠️ @mesh.llm: No 'model' specified for function '{func.__name__}'. "
|
|
1435
|
-
f"The LLM provider will use its default model. "
|
|
1436
|
-
f"Consider specifying a model explicitly (e.g., model='anthropic/claude-sonnet-4-5')."
|
|
1437
|
-
)
|
|
1438
|
-
|
|
1439
|
-
# Use default system prompt if not provided
|
|
1440
|
-
effective_system_prompt = (
|
|
1441
|
-
system_prompt if system_prompt else "You are a helpful assistant."
|
|
1442
|
-
)
|
|
1443
|
-
|
|
1444
1308
|
resolved_config = {
|
|
1445
1309
|
"filter": filter,
|
|
1446
1310
|
"filter_mode": get_config_value(
|
|
@@ -1450,7 +1314,12 @@ def llm(
|
|
|
1450
1314
|
rule=ValidationRule.STRING_RULE,
|
|
1451
1315
|
),
|
|
1452
1316
|
"provider": resolved_provider,
|
|
1453
|
-
"model":
|
|
1317
|
+
"model": get_config_value(
|
|
1318
|
+
"MESH_LLM_MODEL",
|
|
1319
|
+
override=model,
|
|
1320
|
+
default=None,
|
|
1321
|
+
rule=ValidationRule.STRING_RULE,
|
|
1322
|
+
),
|
|
1454
1323
|
"api_key": api_key, # Will be resolved from provider-specific env vars later
|
|
1455
1324
|
"max_iterations": get_config_value(
|
|
1456
1325
|
"MESH_LLM_MAX_ITERATIONS",
|
|
@@ -1458,7 +1327,7 @@ def llm(
|
|
|
1458
1327
|
default=10,
|
|
1459
1328
|
rule=ValidationRule.NONZERO_RULE,
|
|
1460
1329
|
),
|
|
1461
|
-
"system_prompt":
|
|
1330
|
+
"system_prompt": system_prompt,
|
|
1462
1331
|
"system_prompt_file": system_prompt_file,
|
|
1463
1332
|
# Phase 1: Template metadata
|
|
1464
1333
|
"is_template": is_template,
|
mesh/helpers.py
CHANGED
|
@@ -214,58 +214,6 @@ def llm_provider(
|
|
|
214
214
|
f"(requested by consumer)"
|
|
215
215
|
)
|
|
216
216
|
|
|
217
|
-
# Issue #459: Handle output_schema for vendor-specific structured output
|
|
218
|
-
# Convert to response_format for vendors that support it
|
|
219
|
-
output_schema = model_params_copy.pop("output_schema", None)
|
|
220
|
-
output_type_name = model_params_copy.pop("output_type_name", None)
|
|
221
|
-
|
|
222
|
-
# Vendors that support structured output via response_format
|
|
223
|
-
supported_structured_output_vendors = (
|
|
224
|
-
"openai",
|
|
225
|
-
"azure", # Azure OpenAI uses same format as OpenAI
|
|
226
|
-
"gemini",
|
|
227
|
-
"vertex_ai", # Vertex AI Gemini uses same format as Gemini
|
|
228
|
-
"anthropic",
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
if output_schema:
|
|
232
|
-
if vendor in supported_structured_output_vendors:
|
|
233
|
-
# Apply vendor-specific response_format for structured output
|
|
234
|
-
from _mcp_mesh.engine.provider_handlers import make_schema_strict
|
|
235
|
-
|
|
236
|
-
if vendor == "anthropic":
|
|
237
|
-
# Claude: doesn't require all properties in 'required', uses strict=False
|
|
238
|
-
schema = make_schema_strict(
|
|
239
|
-
output_schema, add_all_required=False
|
|
240
|
-
)
|
|
241
|
-
strict_mode = False
|
|
242
|
-
else:
|
|
243
|
-
# OpenAI/Azure/Gemini/Vertex: require all properties in 'required', uses strict=True
|
|
244
|
-
schema = make_schema_strict(
|
|
245
|
-
output_schema, add_all_required=True
|
|
246
|
-
)
|
|
247
|
-
strict_mode = True
|
|
248
|
-
|
|
249
|
-
model_params_copy["response_format"] = {
|
|
250
|
-
"type": "json_schema",
|
|
251
|
-
"json_schema": {
|
|
252
|
-
"name": output_type_name or "Response",
|
|
253
|
-
"schema": schema,
|
|
254
|
-
"strict": strict_mode,
|
|
255
|
-
},
|
|
256
|
-
}
|
|
257
|
-
logger.debug(
|
|
258
|
-
f"🎯 Applied {vendor} response_format for structured output: "
|
|
259
|
-
f"{output_type_name} (strict={strict_mode})"
|
|
260
|
-
)
|
|
261
|
-
else:
|
|
262
|
-
# Vendor doesn't support structured output - warn user
|
|
263
|
-
logger.warning(
|
|
264
|
-
f"⚠️ Structured output schema '{output_type_name or 'Response'}' "
|
|
265
|
-
f"was provided but vendor '{vendor}' does not support response_format. "
|
|
266
|
-
f"The schema will be ignored and the LLM may return unstructured output."
|
|
267
|
-
)
|
|
268
|
-
|
|
269
217
|
# Build litellm.completion arguments
|
|
270
218
|
completion_args: dict[str, Any] = {
|
|
271
219
|
"model": effective_model,
|
mesh/types.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
MCP Mesh type definitions for dependency injection.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import warnings
|
|
6
5
|
from collections.abc import AsyncIterator
|
|
7
6
|
from dataclasses import dataclass
|
|
8
7
|
from typing import Any, Dict, List, Optional, Protocol
|
|
@@ -15,24 +14,24 @@ except ImportError:
|
|
|
15
14
|
PYDANTIC_AVAILABLE = False
|
|
16
15
|
|
|
17
16
|
|
|
18
|
-
class
|
|
17
|
+
class McpMeshAgent(Protocol):
|
|
19
18
|
"""
|
|
20
|
-
MCP Mesh
|
|
19
|
+
Unified MCP Mesh agent proxy using FastMCP's built-in client.
|
|
21
20
|
|
|
22
|
-
This protocol
|
|
23
|
-
|
|
21
|
+
This protocol now provides all MCP protocol features using FastMCP's superior client
|
|
22
|
+
implementation, replacing both the old basic and advanced proxy types.
|
|
24
23
|
|
|
25
24
|
Features:
|
|
26
|
-
-
|
|
27
|
-
- Full MCP protocol methods (tools, resources, prompts)
|
|
25
|
+
- All MCP protocol methods (tools, resources, prompts)
|
|
28
26
|
- Streaming support with FastMCP's StreamableHttpTransport
|
|
29
27
|
- Session management with notifications
|
|
30
|
-
- Automatic redirect handling
|
|
28
|
+
- Automatic redirect handling (fixes /mcp/ → /mcp issue)
|
|
31
29
|
- CallToolResult objects with structured content parsing
|
|
30
|
+
- Enhanced proxy configuration via kwargs
|
|
32
31
|
|
|
33
32
|
Usage Examples:
|
|
34
33
|
@mesh.tool(dependencies=["date-service"])
|
|
35
|
-
def greet(name: str, date_service:
|
|
34
|
+
def greet(name: str, date_service: McpMeshAgent) -> str:
|
|
36
35
|
# Simple call - proxy knows which remote function to invoke
|
|
37
36
|
current_date = date_service()
|
|
38
37
|
|
|
@@ -45,7 +44,7 @@ class McpMeshTool(Protocol):
|
|
|
45
44
|
return f"Hello {name}, today is {current_date}"
|
|
46
45
|
|
|
47
46
|
@mesh.tool(dependencies=["file-service"])
|
|
48
|
-
async def process_files(file_service:
|
|
47
|
+
async def process_files(file_service: McpMeshAgent) -> str:
|
|
49
48
|
# Full MCP Protocol usage
|
|
50
49
|
tools = await file_service.list_tools()
|
|
51
50
|
resources = await file_service.list_resources()
|
|
@@ -63,7 +62,7 @@ class McpMeshTool(Protocol):
|
|
|
63
62
|
|
|
64
63
|
return "Processing complete"
|
|
65
64
|
|
|
66
|
-
The proxy provides all MCP protocol features while maintaining
|
|
65
|
+
The unified proxy provides all MCP protocol features while maintaining simple callable interface.
|
|
67
66
|
"""
|
|
68
67
|
|
|
69
68
|
def __call__(self, arguments: Optional[dict[str, Any]] = None) -> Any:
|
|
@@ -157,15 +156,15 @@ class McpMeshTool(Protocol):
|
|
|
157
156
|
handler: Any,
|
|
158
157
|
) -> core_schema.CoreSchema:
|
|
159
158
|
"""
|
|
160
|
-
Custom Pydantic core schema for
|
|
159
|
+
Custom Pydantic core schema for McpMeshAgent.
|
|
161
160
|
|
|
162
|
-
This makes
|
|
161
|
+
This makes McpMeshAgent parameters appear as optional/nullable in MCP schemas,
|
|
163
162
|
preventing serialization errors while maintaining type safety for dependency injection.
|
|
164
163
|
|
|
165
164
|
The dependency injection system will replace None values with actual proxy objects
|
|
166
165
|
at runtime, so MCP callers never need to provide these parameters.
|
|
167
166
|
"""
|
|
168
|
-
# Treat
|
|
167
|
+
# Treat McpMeshAgent as an optional Any type for MCP serialization
|
|
169
168
|
return core_schema.with_default_schema(
|
|
170
169
|
core_schema.nullable_schema(core_schema.any_schema()),
|
|
171
170
|
default=None,
|
|
@@ -182,32 +181,6 @@ class McpMeshTool(Protocol):
|
|
|
182
181
|
}
|
|
183
182
|
|
|
184
183
|
|
|
185
|
-
def _create_deprecated_mcpmeshagent():
|
|
186
|
-
"""Create McpMeshAgent as a deprecated alias for McpMeshTool."""
|
|
187
|
-
|
|
188
|
-
class McpMeshAgent(McpMeshTool, Protocol):
|
|
189
|
-
"""
|
|
190
|
-
Deprecated: Use McpMeshTool instead.
|
|
191
|
-
|
|
192
|
-
This is a backwards-compatible alias that will be removed in a future version.
|
|
193
|
-
"""
|
|
194
|
-
|
|
195
|
-
def __init_subclass__(cls, **kwargs):
|
|
196
|
-
warnings.warn(
|
|
197
|
-
"McpMeshAgent is deprecated, use McpMeshTool instead. "
|
|
198
|
-
"McpMeshAgent will be removed in a future version.",
|
|
199
|
-
DeprecationWarning,
|
|
200
|
-
stacklevel=2,
|
|
201
|
-
)
|
|
202
|
-
super().__init_subclass__(**kwargs)
|
|
203
|
-
|
|
204
|
-
return McpMeshAgent
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
# Deprecated alias for backwards compatibility
|
|
208
|
-
McpMeshAgent = _create_deprecated_mcpmeshagent()
|
|
209
|
-
|
|
210
|
-
|
|
211
184
|
class MeshLlmAgent(Protocol):
|
|
212
185
|
"""
|
|
213
186
|
LLM agent proxy with automatic agentic loop.
|
|
File without changes
|
|
File without changes
|