mcp-mesh 0.7.20__py3-none-any.whl → 0.8.0__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 +13 -15
- _mcp_mesh/engine/http_wrapper.py +69 -10
- _mcp_mesh/engine/mesh_llm_agent.py +29 -10
- _mcp_mesh/engine/mesh_llm_agent_injector.py +77 -41
- _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
- _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
- _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
- _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
- _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
- _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
- _mcp_mesh/engine/response_parser.py +61 -15
- _mcp_mesh/engine/signature_analyzer.py +58 -68
- _mcp_mesh/engine/unified_mcp_proxy.py +19 -35
- _mcp_mesh/pipeline/__init__.py +9 -20
- _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
- _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
- _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +429 -0
- _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
- _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
- _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
- _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
- _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
- _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
- _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +710 -0
- _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
- _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +31 -8
- _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +23 -11
- _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
- _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
- _mcp_mesh/reload.py +1 -3
- _mcp_mesh/shared/__init__.py +2 -8
- _mcp_mesh/shared/config_resolver.py +124 -80
- _mcp_mesh/shared/defaults.py +89 -14
- _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
- _mcp_mesh/shared/host_resolver.py +8 -46
- _mcp_mesh/shared/server_discovery.py +115 -86
- _mcp_mesh/shared/simple_shutdown.py +44 -86
- _mcp_mesh/tracing/execution_tracer.py +2 -6
- _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
- _mcp_mesh/tracing/trace_context_helper.py +3 -13
- _mcp_mesh/tracing/utils.py +29 -15
- _mcp_mesh/utils/fastmcp_schema_extractor.py +5 -4
- {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/METADATA +7 -5
- mcp_mesh-0.8.0.dist-info/RECORD +85 -0
- mesh/__init__.py +12 -1
- mesh/decorators.py +248 -33
- mesh/helpers.py +52 -0
- mesh/types.py +40 -13
- _mcp_mesh/generated/.openapi-generator/FILES +0 -50
- _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
- _mcp_mesh/generated/.openapi-generator-ignore +0 -15
- _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
- _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
- _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
- _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
- _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
- _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
- _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
- _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
- _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
- _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
- _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
- _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
- _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
- _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
- _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
- _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
- _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
- _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -247
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
- _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
- _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
- _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
- _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
- _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
- _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
- _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
- _mcp_mesh/shared/registry_client_wrapper.py +0 -515
- mcp_mesh-0.7.20.dist-info/RECORD +0 -152
- {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/licenses/LICENSE +0 -0
mesh/decorators.py
CHANGED
|
@@ -26,6 +26,25 @@ _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
|
+
|
|
29
48
|
def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
30
49
|
"""
|
|
31
50
|
Start basic uvicorn server immediately to prevent Python interpreter shutdown.
|
|
@@ -76,8 +95,75 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
76
95
|
app = FastAPI(title="MCP Mesh Agent (Starting)")
|
|
77
96
|
logger.debug("📦 IMMEDIATE UVICORN: Created minimal FastAPI app")
|
|
78
97
|
|
|
79
|
-
# Add trace
|
|
80
|
-
# This must be done
|
|
98
|
+
# Add middleware to strip trace arguments from tool calls BEFORE app starts
|
|
99
|
+
# This must be done unconditionally because meshctl --trace sends trace args
|
|
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
|
|
81
167
|
try:
|
|
82
168
|
import os
|
|
83
169
|
|
|
@@ -90,9 +176,10 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
90
176
|
"""Pure ASGI middleware for trace context and header injection.
|
|
91
177
|
|
|
92
178
|
This middleware:
|
|
93
|
-
1. Extracts trace context from incoming request headers
|
|
94
|
-
2.
|
|
95
|
-
3.
|
|
179
|
+
1. Extracts trace context from incoming request headers AND arguments
|
|
180
|
+
2. Strips trace fields (_trace_id, _parent_span) from arguments to avoid validation errors
|
|
181
|
+
3. Sets up trace context for the request lifecycle
|
|
182
|
+
4. Injects trace headers into the response (works with SSE)
|
|
96
183
|
"""
|
|
97
184
|
|
|
98
185
|
def __init__(self, app):
|
|
@@ -127,7 +214,7 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
127
214
|
headers_list, "x-parent-span"
|
|
128
215
|
)
|
|
129
216
|
|
|
130
|
-
# Setup trace context
|
|
217
|
+
# Setup trace context from headers
|
|
131
218
|
trace_context = {
|
|
132
219
|
"trace_id": (
|
|
133
220
|
incoming_trace_id if incoming_trace_id else None
|
|
@@ -151,6 +238,72 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
151
238
|
except Exception as e:
|
|
152
239
|
logger.warning(f"Failed to set trace context: {e}")
|
|
153
240
|
|
|
241
|
+
# Create receive wrapper to extract trace context from arguments
|
|
242
|
+
# Note: Argument stripping is handled by TraceArgumentStripperMiddleware
|
|
243
|
+
import json as json_module
|
|
244
|
+
|
|
245
|
+
async def receive_with_trace_extraction():
|
|
246
|
+
message = await receive()
|
|
247
|
+
if message["type"] == "http.request":
|
|
248
|
+
body = message.get("body", b"")
|
|
249
|
+
if body:
|
|
250
|
+
try:
|
|
251
|
+
payload = json_module.loads(
|
|
252
|
+
body.decode("utf-8")
|
|
253
|
+
)
|
|
254
|
+
if payload.get("method") == "tools/call":
|
|
255
|
+
arguments = payload.get("params", {}).get(
|
|
256
|
+
"arguments", {}
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Extract trace context from arguments if not in headers
|
|
260
|
+
nonlocal trace_id, span_id, parent_span
|
|
261
|
+
if not trace_id and arguments.get(
|
|
262
|
+
"_trace_id"
|
|
263
|
+
):
|
|
264
|
+
try:
|
|
265
|
+
from _mcp_mesh.tracing.context import (
|
|
266
|
+
TraceContext,
|
|
267
|
+
)
|
|
268
|
+
from _mcp_mesh.tracing.trace_context_helper import (
|
|
269
|
+
TraceContextHelper,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
arg_trace_id = arguments.get(
|
|
273
|
+
"_trace_id"
|
|
274
|
+
)
|
|
275
|
+
arg_parent_span = arguments.get(
|
|
276
|
+
"_parent_span"
|
|
277
|
+
)
|
|
278
|
+
trace_context = {
|
|
279
|
+
"trace_id": arg_trace_id,
|
|
280
|
+
"parent_span": arg_parent_span,
|
|
281
|
+
}
|
|
282
|
+
TraceContextHelper.setup_request_trace_context(
|
|
283
|
+
trace_context, logger
|
|
284
|
+
)
|
|
285
|
+
current_trace = (
|
|
286
|
+
TraceContext.get_current()
|
|
287
|
+
)
|
|
288
|
+
if current_trace:
|
|
289
|
+
trace_id = (
|
|
290
|
+
current_trace.trace_id
|
|
291
|
+
)
|
|
292
|
+
span_id = current_trace.span_id
|
|
293
|
+
parent_span = (
|
|
294
|
+
current_trace.parent_span
|
|
295
|
+
)
|
|
296
|
+
logger.debug(
|
|
297
|
+
f"[TRACE] Extracted trace context from arguments: trace_id={arg_trace_id}"
|
|
298
|
+
)
|
|
299
|
+
except Exception:
|
|
300
|
+
pass
|
|
301
|
+
except Exception as e:
|
|
302
|
+
logger.debug(
|
|
303
|
+
f"[TRACE] Failed to process body for extraction: {e}"
|
|
304
|
+
)
|
|
305
|
+
return message
|
|
306
|
+
|
|
154
307
|
# Wrap send to inject headers before response starts
|
|
155
308
|
async def send_with_trace_headers(message):
|
|
156
309
|
if message["type"] == "http.response.start" and trace_id:
|
|
@@ -166,7 +319,9 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
166
319
|
message = {**message, "headers": headers}
|
|
167
320
|
await send(message)
|
|
168
321
|
|
|
169
|
-
await self.app(
|
|
322
|
+
await self.app(
|
|
323
|
+
scope, receive_with_trace_extraction, send_with_trace_headers
|
|
324
|
+
)
|
|
170
325
|
|
|
171
326
|
app.add_middleware(TraceContextMiddleware)
|
|
172
327
|
logger.debug(
|
|
@@ -215,8 +370,19 @@ def _start_uvicorn_immediately(http_host: str, http_port: int):
|
|
|
215
370
|
|
|
216
371
|
logger.debug("📦 IMMEDIATE UVICORN: Added status endpoints")
|
|
217
372
|
|
|
218
|
-
#
|
|
219
|
-
|
|
373
|
+
# Port handling:
|
|
374
|
+
# - http_port=0 explicitly means auto-assign (let uvicorn choose)
|
|
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")
|
|
220
386
|
|
|
221
387
|
logger.debug(
|
|
222
388
|
f"🚀 IMMEDIATE UVICORN: Starting uvicorn server on {http_host}:{port}"
|
|
@@ -452,12 +618,25 @@ def tool(
|
|
|
452
618
|
raise ValueError("dependency capability must be a string")
|
|
453
619
|
|
|
454
620
|
# 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)
|
|
455
623
|
dep_tags = dep.get("tags", [])
|
|
456
624
|
if not isinstance(dep_tags, list):
|
|
457
625
|
raise ValueError("dependency tags must be a list")
|
|
458
626
|
for tag in dep_tags:
|
|
459
|
-
if
|
|
460
|
-
|
|
627
|
+
if isinstance(tag, str):
|
|
628
|
+
continue # Simple tag - OK
|
|
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
|
+
)
|
|
461
640
|
|
|
462
641
|
dep_version = dep.get("version")
|
|
463
642
|
if dep_version is not None and not isinstance(dep_version, str):
|
|
@@ -575,7 +754,7 @@ def agent(
|
|
|
575
754
|
http_port: int = 0,
|
|
576
755
|
enable_http: bool = True,
|
|
577
756
|
namespace: str = "default",
|
|
578
|
-
|
|
757
|
+
heartbeat_interval: int = 5,
|
|
579
758
|
health_check: Callable[[], Awaitable[Any]] | None = None,
|
|
580
759
|
health_check_ttl: int = 15,
|
|
581
760
|
auto_run: bool = True, # Changed to True by default!
|
|
@@ -600,7 +779,7 @@ def agent(
|
|
|
600
779
|
Environment variable: MCP_MESH_HTTP_ENABLED (takes precedence)
|
|
601
780
|
namespace: Agent namespace (default: "default")
|
|
602
781
|
Environment variable: MCP_MESH_NAMESPACE (takes precedence)
|
|
603
|
-
|
|
782
|
+
heartbeat_interval: Heartbeat interval in seconds (default: 5)
|
|
604
783
|
Environment variable: MCP_MESH_HEALTH_INTERVAL (takes precedence)
|
|
605
784
|
health_check: Optional async function that returns HealthStatus
|
|
606
785
|
Called before heartbeat and on /health endpoint with TTL caching
|
|
@@ -617,7 +796,7 @@ def agent(
|
|
|
617
796
|
MCP_MESH_HTTP_PORT: Override http_port parameter (integer, 0-65535)
|
|
618
797
|
MCP_MESH_HTTP_ENABLED: Override enable_http parameter (boolean: true/false)
|
|
619
798
|
MCP_MESH_NAMESPACE: Override namespace parameter (string)
|
|
620
|
-
MCP_MESH_HEALTH_INTERVAL: Override
|
|
799
|
+
MCP_MESH_HEALTH_INTERVAL: Override heartbeat_interval parameter (integer, ≥1)
|
|
621
800
|
MCP_MESH_AUTO_RUN: Override auto_run parameter (boolean: true/false)
|
|
622
801
|
MCP_MESH_AUTO_RUN_INTERVAL: Override auto_run_interval parameter (integer, ≥1)
|
|
623
802
|
|
|
@@ -668,10 +847,10 @@ def agent(
|
|
|
668
847
|
if not isinstance(namespace, str):
|
|
669
848
|
raise ValueError("namespace must be a string")
|
|
670
849
|
|
|
671
|
-
if not isinstance(
|
|
672
|
-
raise ValueError("
|
|
673
|
-
if
|
|
674
|
-
raise ValueError("
|
|
850
|
+
if not isinstance(heartbeat_interval, int):
|
|
851
|
+
raise ValueError("heartbeat_interval must be an integer")
|
|
852
|
+
if heartbeat_interval < 1:
|
|
853
|
+
raise ValueError("heartbeat_interval must be at least 1 second")
|
|
675
854
|
|
|
676
855
|
if not isinstance(auto_run, bool):
|
|
677
856
|
raise ValueError("auto_run must be a boolean")
|
|
@@ -726,9 +905,9 @@ def agent(
|
|
|
726
905
|
# Import centralized defaults
|
|
727
906
|
from _mcp_mesh.shared.defaults import MeshDefaults
|
|
728
907
|
|
|
729
|
-
|
|
908
|
+
final_heartbeat_interval = get_config_value(
|
|
730
909
|
"MCP_MESH_HEALTH_INTERVAL",
|
|
731
|
-
override=
|
|
910
|
+
override=heartbeat_interval,
|
|
732
911
|
default=MeshDefaults.HEALTH_INTERVAL,
|
|
733
912
|
rule=ValidationRule.NONZERO_RULE,
|
|
734
913
|
)
|
|
@@ -759,7 +938,7 @@ def agent(
|
|
|
759
938
|
"http_port": final_http_port,
|
|
760
939
|
"enable_http": final_enable_http,
|
|
761
940
|
"namespace": final_namespace,
|
|
762
|
-
"
|
|
941
|
+
"heartbeat_interval": final_heartbeat_interval,
|
|
763
942
|
"health_check": health_check,
|
|
764
943
|
"health_check_ttl": health_check_ttl,
|
|
765
944
|
"auto_run": final_auto_run,
|
|
@@ -898,10 +1077,10 @@ def route(
|
|
|
898
1077
|
async def upload_resume(
|
|
899
1078
|
request: Request,
|
|
900
1079
|
file: UploadFile = File(...),
|
|
901
|
-
|
|
902
|
-
user_service: mesh.
|
|
1080
|
+
pdf_tool: mesh.McpMeshTool = None, # Injected by MCP Mesh
|
|
1081
|
+
user_service: mesh.McpMeshTool = None # Injected by MCP Mesh
|
|
903
1082
|
):
|
|
904
|
-
result = await
|
|
1083
|
+
result = await pdf_tool.extract_text_from_pdf(file)
|
|
905
1084
|
await user_service.update_profile(user_data, result)
|
|
906
1085
|
return {"success": True}
|
|
907
1086
|
"""
|
|
@@ -930,12 +1109,25 @@ def route(
|
|
|
930
1109
|
raise ValueError("dependency capability must be a string")
|
|
931
1110
|
|
|
932
1111
|
# 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)
|
|
933
1114
|
dep_tags = dep.get("tags", [])
|
|
934
1115
|
if not isinstance(dep_tags, list):
|
|
935
1116
|
raise ValueError("dependency tags must be a list")
|
|
936
1117
|
for tag in dep_tags:
|
|
937
|
-
if
|
|
938
|
-
|
|
1118
|
+
if isinstance(tag, str):
|
|
1119
|
+
continue # Simple tag - OK
|
|
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
|
+
)
|
|
939
1131
|
|
|
940
1132
|
dep_version = dep.get("version")
|
|
941
1133
|
if dep_version is not None and not isinstance(dep_version, str):
|
|
@@ -1221,6 +1413,34 @@ def llm(
|
|
|
1221
1413
|
rule=ValidationRule.STRING_RULE,
|
|
1222
1414
|
)
|
|
1223
1415
|
|
|
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
|
+
|
|
1224
1444
|
resolved_config = {
|
|
1225
1445
|
"filter": filter,
|
|
1226
1446
|
"filter_mode": get_config_value(
|
|
@@ -1230,12 +1450,7 @@ def llm(
|
|
|
1230
1450
|
rule=ValidationRule.STRING_RULE,
|
|
1231
1451
|
),
|
|
1232
1452
|
"provider": resolved_provider,
|
|
1233
|
-
"model":
|
|
1234
|
-
"MESH_LLM_MODEL",
|
|
1235
|
-
override=model,
|
|
1236
|
-
default=None,
|
|
1237
|
-
rule=ValidationRule.STRING_RULE,
|
|
1238
|
-
),
|
|
1453
|
+
"model": resolved_model,
|
|
1239
1454
|
"api_key": api_key, # Will be resolved from provider-specific env vars later
|
|
1240
1455
|
"max_iterations": get_config_value(
|
|
1241
1456
|
"MESH_LLM_MAX_ITERATIONS",
|
|
@@ -1243,7 +1458,7 @@ def llm(
|
|
|
1243
1458
|
default=10,
|
|
1244
1459
|
rule=ValidationRule.NONZERO_RULE,
|
|
1245
1460
|
),
|
|
1246
|
-
"system_prompt":
|
|
1461
|
+
"system_prompt": effective_system_prompt,
|
|
1247
1462
|
"system_prompt_file": system_prompt_file,
|
|
1248
1463
|
# Phase 1: Template metadata
|
|
1249
1464
|
"is_template": is_template,
|
mesh/helpers.py
CHANGED
|
@@ -214,6 +214,58 @@ 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
|
+
|
|
217
269
|
# Build litellm.completion arguments
|
|
218
270
|
completion_args: dict[str, Any] = {
|
|
219
271
|
"model": effective_model,
|
mesh/types.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
MCP Mesh type definitions for dependency injection.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import warnings
|
|
5
6
|
from collections.abc import AsyncIterator
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from typing import Any, Dict, List, Optional, Protocol
|
|
@@ -14,24 +15,24 @@ except ImportError:
|
|
|
14
15
|
PYDANTIC_AVAILABLE = False
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
class
|
|
18
|
+
class McpMeshTool(Protocol):
|
|
18
19
|
"""
|
|
19
|
-
|
|
20
|
+
MCP Mesh tool proxy for dependency injection.
|
|
20
21
|
|
|
21
|
-
This protocol
|
|
22
|
-
|
|
22
|
+
This protocol defines the interface for injected tool dependencies. When you declare
|
|
23
|
+
a dependency on a remote tool, MCP Mesh injects a proxy that implements this interface.
|
|
23
24
|
|
|
24
25
|
Features:
|
|
25
|
-
-
|
|
26
|
+
- Simple callable interface for tool invocation
|
|
27
|
+
- Full MCP protocol methods (tools, resources, prompts)
|
|
26
28
|
- Streaming support with FastMCP's StreamableHttpTransport
|
|
27
29
|
- Session management with notifications
|
|
28
|
-
- Automatic redirect handling
|
|
30
|
+
- Automatic redirect handling
|
|
29
31
|
- CallToolResult objects with structured content parsing
|
|
30
|
-
- Enhanced proxy configuration via kwargs
|
|
31
32
|
|
|
32
33
|
Usage Examples:
|
|
33
34
|
@mesh.tool(dependencies=["date-service"])
|
|
34
|
-
def greet(name: str, date_service:
|
|
35
|
+
def greet(name: str, date_service: McpMeshTool) -> str:
|
|
35
36
|
# Simple call - proxy knows which remote function to invoke
|
|
36
37
|
current_date = date_service()
|
|
37
38
|
|
|
@@ -44,7 +45,7 @@ class McpMeshAgent(Protocol):
|
|
|
44
45
|
return f"Hello {name}, today is {current_date}"
|
|
45
46
|
|
|
46
47
|
@mesh.tool(dependencies=["file-service"])
|
|
47
|
-
async def process_files(file_service:
|
|
48
|
+
async def process_files(file_service: McpMeshTool) -> str:
|
|
48
49
|
# Full MCP Protocol usage
|
|
49
50
|
tools = await file_service.list_tools()
|
|
50
51
|
resources = await file_service.list_resources()
|
|
@@ -62,7 +63,7 @@ class McpMeshAgent(Protocol):
|
|
|
62
63
|
|
|
63
64
|
return "Processing complete"
|
|
64
65
|
|
|
65
|
-
The
|
|
66
|
+
The proxy provides all MCP protocol features while maintaining a simple callable interface.
|
|
66
67
|
"""
|
|
67
68
|
|
|
68
69
|
def __call__(self, arguments: Optional[dict[str, Any]] = None) -> Any:
|
|
@@ -156,15 +157,15 @@ class McpMeshAgent(Protocol):
|
|
|
156
157
|
handler: Any,
|
|
157
158
|
) -> core_schema.CoreSchema:
|
|
158
159
|
"""
|
|
159
|
-
Custom Pydantic core schema for
|
|
160
|
+
Custom Pydantic core schema for McpMeshTool.
|
|
160
161
|
|
|
161
|
-
This makes
|
|
162
|
+
This makes McpMeshTool parameters appear as optional/nullable in MCP schemas,
|
|
162
163
|
preventing serialization errors while maintaining type safety for dependency injection.
|
|
163
164
|
|
|
164
165
|
The dependency injection system will replace None values with actual proxy objects
|
|
165
166
|
at runtime, so MCP callers never need to provide these parameters.
|
|
166
167
|
"""
|
|
167
|
-
# Treat
|
|
168
|
+
# Treat McpMeshTool as an optional Any type for MCP serialization
|
|
168
169
|
return core_schema.with_default_schema(
|
|
169
170
|
core_schema.nullable_schema(core_schema.any_schema()),
|
|
170
171
|
default=None,
|
|
@@ -181,6 +182,32 @@ class McpMeshAgent(Protocol):
|
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
|
|
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
|
+
|
|
184
211
|
class MeshLlmAgent(Protocol):
|
|
185
212
|
"""
|
|
186
213
|
LLM agent proxy with automatic agentic loop.
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
mcp_mesh_registry_client/__init__.py
|
|
2
|
-
mcp_mesh_registry_client/api/__init__.py
|
|
3
|
-
mcp_mesh_registry_client/api/agents_api.py
|
|
4
|
-
mcp_mesh_registry_client/api/health_api.py
|
|
5
|
-
mcp_mesh_registry_client/api/tracing_api.py
|
|
6
|
-
mcp_mesh_registry_client/api_client.py
|
|
7
|
-
mcp_mesh_registry_client/api_response.py
|
|
8
|
-
mcp_mesh_registry_client/configuration.py
|
|
9
|
-
mcp_mesh_registry_client/exceptions.py
|
|
10
|
-
mcp_mesh_registry_client/models/__init__.py
|
|
11
|
-
mcp_mesh_registry_client/models/agent_info.py
|
|
12
|
-
mcp_mesh_registry_client/models/agent_metadata.py
|
|
13
|
-
mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py
|
|
14
|
-
mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py
|
|
15
|
-
mcp_mesh_registry_client/models/agent_registration.py
|
|
16
|
-
mcp_mesh_registry_client/models/agent_registration_metadata.py
|
|
17
|
-
mcp_mesh_registry_client/models/agents_list_response.py
|
|
18
|
-
mcp_mesh_registry_client/models/capability_info.py
|
|
19
|
-
mcp_mesh_registry_client/models/decorator_agent_metadata.py
|
|
20
|
-
mcp_mesh_registry_client/models/decorator_agent_request.py
|
|
21
|
-
mcp_mesh_registry_client/models/decorator_info.py
|
|
22
|
-
mcp_mesh_registry_client/models/dependency_info.py
|
|
23
|
-
mcp_mesh_registry_client/models/dependency_resolution_info.py
|
|
24
|
-
mcp_mesh_registry_client/models/error_response.py
|
|
25
|
-
mcp_mesh_registry_client/models/health_response.py
|
|
26
|
-
mcp_mesh_registry_client/models/heartbeat_request.py
|
|
27
|
-
mcp_mesh_registry_client/models/heartbeat_request_metadata.py
|
|
28
|
-
mcp_mesh_registry_client/models/heartbeat_response.py
|
|
29
|
-
mcp_mesh_registry_client/models/llm_provider.py
|
|
30
|
-
mcp_mesh_registry_client/models/llm_provider_resolution_info.py
|
|
31
|
-
mcp_mesh_registry_client/models/llm_tool_filter.py
|
|
32
|
-
mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py
|
|
33
|
-
mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py
|
|
34
|
-
mcp_mesh_registry_client/models/llm_tool_info.py
|
|
35
|
-
mcp_mesh_registry_client/models/llm_tool_resolution_info.py
|
|
36
|
-
mcp_mesh_registry_client/models/mesh_agent_register_metadata.py
|
|
37
|
-
mcp_mesh_registry_client/models/mesh_agent_registration.py
|
|
38
|
-
mcp_mesh_registry_client/models/mesh_registration_response.py
|
|
39
|
-
mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py
|
|
40
|
-
mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py
|
|
41
|
-
mcp_mesh_registry_client/models/mesh_tool_register_metadata.py
|
|
42
|
-
mcp_mesh_registry_client/models/mesh_tool_registration.py
|
|
43
|
-
mcp_mesh_registry_client/models/registration_response.py
|
|
44
|
-
mcp_mesh_registry_client/models/resolved_llm_provider.py
|
|
45
|
-
mcp_mesh_registry_client/models/rich_dependency.py
|
|
46
|
-
mcp_mesh_registry_client/models/root_response.py
|
|
47
|
-
mcp_mesh_registry_client/models/standardized_dependency.py
|
|
48
|
-
mcp_mesh_registry_client/models/trace_event.py
|
|
49
|
-
mcp_mesh_registry_client/py.typed
|
|
50
|
-
mcp_mesh_registry_client/rest.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
7.13.0
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Skip packaging files - we use this as part of our main package
|
|
2
|
-
pyproject.toml
|
|
3
|
-
setup.py
|
|
4
|
-
setup.cfg
|
|
5
|
-
requirements.txt
|
|
6
|
-
test-requirements.txt
|
|
7
|
-
README.md
|
|
8
|
-
tox.ini
|
|
9
|
-
.travis.yml
|
|
10
|
-
.gitlab-ci.yml
|
|
11
|
-
.github/
|
|
12
|
-
git_push.sh
|
|
13
|
-
.gitignore
|
|
14
|
-
test/
|
|
15
|
-
docs/
|