mcp-mesh 0.7.21__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.
Files changed (124) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/dependency_injector.py +13 -15
  3. _mcp_mesh/engine/http_wrapper.py +69 -10
  4. _mcp_mesh/engine/mesh_llm_agent.py +29 -10
  5. _mcp_mesh/engine/mesh_llm_agent_injector.py +77 -41
  6. _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
  7. _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
  8. _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
  9. _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
  10. _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
  11. _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
  12. _mcp_mesh/engine/response_parser.py +61 -15
  13. _mcp_mesh/engine/signature_analyzer.py +58 -68
  14. _mcp_mesh/engine/unified_mcp_proxy.py +19 -35
  15. _mcp_mesh/pipeline/__init__.py +9 -20
  16. _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
  17. _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
  18. _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +429 -0
  19. _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
  20. _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
  21. _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
  22. _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
  23. _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
  24. _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
  25. _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +710 -0
  26. _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
  27. _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
  28. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +31 -8
  29. _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
  30. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +23 -11
  31. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
  32. _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
  33. _mcp_mesh/reload.py +1 -3
  34. _mcp_mesh/shared/__init__.py +2 -8
  35. _mcp_mesh/shared/config_resolver.py +124 -80
  36. _mcp_mesh/shared/defaults.py +89 -14
  37. _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
  38. _mcp_mesh/shared/host_resolver.py +8 -46
  39. _mcp_mesh/shared/server_discovery.py +115 -86
  40. _mcp_mesh/shared/simple_shutdown.py +44 -86
  41. _mcp_mesh/tracing/execution_tracer.py +2 -6
  42. _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
  43. _mcp_mesh/tracing/trace_context_helper.py +3 -13
  44. _mcp_mesh/tracing/utils.py +29 -15
  45. _mcp_mesh/utils/fastmcp_schema_extractor.py +5 -4
  46. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0.dist-info}/METADATA +7 -5
  47. mcp_mesh-0.8.0.dist-info/RECORD +85 -0
  48. mesh/__init__.py +12 -1
  49. mesh/decorators.py +248 -33
  50. mesh/helpers.py +52 -0
  51. mesh/types.py +40 -13
  52. _mcp_mesh/generated/.openapi-generator/FILES +0 -50
  53. _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
  54. _mcp_mesh/generated/.openapi-generator-ignore +0 -15
  55. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
  56. _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
  57. _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
  58. _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
  59. _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
  60. _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
  61. _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
  62. _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
  63. _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
  64. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
  65. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
  66. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
  67. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
  68. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
  69. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
  70. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
  71. _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
  72. _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
  73. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
  74. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
  75. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
  76. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
  77. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
  78. _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
  79. _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
  80. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
  81. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
  82. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
  83. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
  84. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
  85. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
  86. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
  87. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
  88. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
  89. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
  90. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
  91. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
  92. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
  93. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
  94. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
  95. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
  96. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
  97. _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
  98. _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
  99. _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
  100. _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
  101. _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
  102. _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
  103. _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
  104. _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
  105. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
  106. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
  107. _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
  108. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -243
  109. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
  110. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
  111. _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
  112. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
  113. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
  114. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
  115. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
  116. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
  117. _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
  118. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
  119. _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
  120. _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
  121. _mcp_mesh/shared/registry_client_wrapper.py +0 -515
  122. mcp_mesh-0.7.21.dist-info/RECORD +0 -152
  123. {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0.dist-info}/WHEEL +0 -0
  124. {mcp_mesh-0.7.21.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 context middleware for distributed tracing BEFORE app starts
80
- # This must be done before uvicorn.run() since middleware can't be added after start
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. Sets up trace context for the request lifecycle
95
- 3. Injects trace headers into the response (works with SSE)
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(scope, receive, send_with_trace_headers)
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
- # Determine port (0 means auto-assign)
219
- port = http_port if http_port > 0 else 8080
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 not isinstance(tag, str):
460
- raise ValueError("all dependency tags must be strings")
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
- health_interval: int = 5, # Will be overridden by centralized defaults
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
- health_interval: Health check interval in seconds (default: 30)
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 health_interval parameter (integer, ≥1)
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(health_interval, int):
672
- raise ValueError("health_interval must be an integer")
673
- if health_interval < 1:
674
- raise ValueError("health_interval must be at least 1 second")
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
- final_health_interval = get_config_value(
908
+ final_heartbeat_interval = get_config_value(
730
909
  "MCP_MESH_HEALTH_INTERVAL",
731
- override=health_interval,
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
- "health_interval": final_health_interval,
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
- pdf_agent: mesh.McpMeshAgent = None, # Injected by MCP Mesh
902
- user_service: mesh.McpMeshAgent = None # Injected by MCP 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 pdf_agent.extract_text_from_pdf(file)
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 not isinstance(tag, str):
938
- raise ValueError("all dependency tags must be strings")
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": get_config_value(
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": 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 McpMeshAgent(Protocol):
18
+ class McpMeshTool(Protocol):
18
19
  """
19
- Unified MCP Mesh agent proxy using FastMCP's built-in client.
20
+ MCP Mesh tool proxy for dependency injection.
20
21
 
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.
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
- - All MCP protocol methods (tools, resources, prompts)
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 (fixes /mcp/ → /mcp issue)
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: McpMeshAgent) -> str:
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: McpMeshAgent) -> str:
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 unified proxy provides all MCP protocol features while maintaining simple callable interface.
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 McpMeshAgent.
160
+ Custom Pydantic core schema for McpMeshTool.
160
161
 
161
- This makes McpMeshAgent parameters appear as optional/nullable in MCP schemas,
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 McpMeshAgent as an optional Any type for MCP serialization
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/