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
_mcp_mesh/__init__.py CHANGED
@@ -31,7 +31,7 @@ from .engine.decorator_registry import (
31
31
  get_decorator_stats,
32
32
  )
33
33
 
34
- __version__ = "0.7.21"
34
+ __version__ = "0.8.0"
35
35
 
36
36
  # Store reference to runtime processor if initialized
37
37
  _runtime_processor = None
@@ -14,12 +14,10 @@ import weakref
14
14
  from collections.abc import Callable
15
15
  from typing import Any
16
16
 
17
- from ..shared.logging_config import (
18
- format_log_value,
19
- format_result_summary,
20
- get_trace_prefix,
21
- )
22
- from .signature_analyzer import get_mesh_agent_positions, has_llm_agent_parameter
17
+ from ..shared.logging_config import (format_log_value, format_result_summary,
18
+ get_trace_prefix)
19
+ from .signature_analyzer import (get_mesh_agent_positions,
20
+ has_llm_agent_parameter)
23
21
 
24
22
  logger = logging.getLogger(__name__)
25
23
 
@@ -29,8 +27,8 @@ def analyze_injection_strategy(func: Callable, dependencies: list[str]) -> list[
29
27
  Analyze function signature and determine injection strategy.
30
28
 
31
29
  Rules:
32
- 1. Single parameter: inject regardless of typing (with warning if not McpMeshAgent)
33
- 2. Multiple parameters: only inject into McpMeshAgent typed parameters
30
+ 1. Single parameter: inject regardless of typing (with warning if not McpMeshTool)
31
+ 2. Multiple parameters: only inject into McpMeshTool typed parameters
34
32
  3. Log warnings for mismatches and edge cases
35
33
 
36
34
  Args:
@@ -62,17 +60,17 @@ def analyze_injection_strategy(func: Callable, dependencies: list[str]) -> list[
62
60
  logger.warning(
63
61
  f"Single parameter '{param_name}' in function '{func_name}' found, "
64
62
  f"injecting {dependencies[0] if dependencies else 'dependency'} proxy "
65
- f"(consider typing as McpMeshAgent for clarity)"
63
+ f"(consider typing as McpMeshTool for clarity)"
66
64
  )
67
65
  return [0] # Inject into the single parameter
68
66
 
69
- # Multiple parameters rule: only inject into McpMeshAgent typed parameters
67
+ # Multiple parameters rule: only inject into McpMeshTool typed parameters
70
68
  if param_count > 1:
71
69
  if not mesh_positions:
72
70
  logger.warning(
73
71
  f"⚠️ Function '{func_name}' has {param_count} parameters but none are "
74
- f"typed as McpMeshAgent. Skipping injection of {len(dependencies)} dependencies. "
75
- f"Consider typing dependency parameters as McpMeshAgent."
72
+ f"typed as McpMeshTool. Skipping injection of {len(dependencies)} dependencies. "
73
+ f"Consider typing dependency parameters as McpMeshTool."
76
74
  )
77
75
  return []
78
76
 
@@ -82,7 +80,7 @@ def analyze_injection_strategy(func: Callable, dependencies: list[str]) -> list[
82
80
  excess_deps = dependencies[len(mesh_positions) :]
83
81
  logger.warning(
84
82
  f"Function '{func_name}' has {len(dependencies)} dependencies "
85
- f"but only {len(mesh_positions)} McpMeshAgent parameters. "
83
+ f"but only {len(mesh_positions)} McpMeshTool parameters. "
86
84
  f"Dependencies {excess_deps} will not be injected."
87
85
  )
88
86
  else:
@@ -90,7 +88,7 @@ def analyze_injection_strategy(func: Callable, dependencies: list[str]) -> list[
90
88
  params[pos].name for pos in mesh_positions[len(dependencies) :]
91
89
  ]
92
90
  logger.warning(
93
- f"Function '{func_name}' has {len(mesh_positions)} McpMeshAgent parameters "
91
+ f"Function '{func_name}' has {len(mesh_positions)} McpMeshTool parameters "
94
92
  f"but only {len(dependencies)} dependencies declared. "
95
93
  f"Parameters {excess_params} will remain None."
96
94
  )
@@ -106,7 +104,7 @@ class DependencyInjector:
106
104
  Manages dynamic dependency injection for mesh agents.
107
105
 
108
106
  This class:
109
- 1. Maintains a registry of available dependencies (McpMeshAgent)
107
+ 1. Maintains a registry of available dependencies (McpMeshTool)
110
108
  2. Coordinates with MeshLlmAgentInjector for LLM agent injection
111
109
  3. Tracks which functions depend on which services
112
110
  4. Updates function bindings when topology changes
@@ -387,8 +387,13 @@ class HttpMcpWrapper:
387
387
  self.logger = logger
388
388
 
389
389
  async def dispatch(self, request: Request, call_next):
390
- # Extract and set trace context from headers for distributed tracing
390
+ # Read body once for processing
391
+ body = await request.body()
392
+ modified_body = body
393
+
394
+ # Extract and set trace context from headers and arguments
391
395
  try:
396
+ from ..tracing.context import TraceContext
392
397
  from ..tracing.trace_context_helper import TraceContextHelper
393
398
 
394
399
  # DEBUG: Log incoming headers for trace propagation debugging
@@ -399,22 +404,67 @@ class HttpMcpWrapper:
399
404
  f"X-Parent-Span={parent_span_header}, path={request.url.path}"
400
405
  )
401
406
 
402
- # Use helper class for trace context extraction and setup
403
- trace_context = (
404
- await TraceContextHelper.extract_trace_context_from_request(
405
- request
407
+ # Extract trace context from both headers AND arguments
408
+ trace_id = trace_id_header
409
+ parent_span = parent_span_header
410
+
411
+ # Try extracting from JSON-RPC body arguments as fallback
412
+ # Also strip trace fields from arguments to avoid Pydantic validation errors
413
+ if body:
414
+ try:
415
+ payload = json.loads(body.decode("utf-8"))
416
+ if payload.get("method") == "tools/call":
417
+ arguments = payload.get("params", {}).get(
418
+ "arguments", {}
419
+ )
420
+
421
+ # Extract trace context from arguments (TypeScript uses _trace_id/_parent_span)
422
+ if not trace_id and arguments.get("_trace_id"):
423
+ trace_id = arguments.get("_trace_id")
424
+ if not parent_span and arguments.get("_parent_span"):
425
+ parent_span = arguments.get("_parent_span")
426
+
427
+ # Strip trace context fields from arguments before passing to FastMCP
428
+ if (
429
+ "_trace_id" in arguments
430
+ or "_parent_span" in arguments
431
+ ):
432
+ arguments.pop("_trace_id", None)
433
+ arguments.pop("_parent_span", None)
434
+ # Update payload with cleaned arguments
435
+ modified_body = json.dumps(payload).encode("utf-8")
436
+ self.logger.debug(
437
+ f"🔗 Stripped trace fields from arguments, "
438
+ f"trace_id={trace_id[:8] if trace_id else None}..."
439
+ )
440
+ except Exception as e:
441
+ self.logger.debug(
442
+ f"Failed to process body for trace context: {e}"
443
+ )
444
+
445
+ # Setup trace context if we have a trace_id
446
+ if trace_id:
447
+ trace_context = {
448
+ "trace_id": trace_id,
449
+ "parent_span": parent_span,
450
+ }
451
+ TraceContextHelper.setup_request_trace_context(
452
+ trace_context, self.logger
406
453
  )
407
- )
408
- TraceContextHelper.setup_request_trace_context(
409
- trace_context, self.logger
410
- )
411
454
  except Exception as e:
412
455
  # Never fail request due to tracing issues
413
456
  self.logger.warning(f"Failed to set trace context: {e}")
414
457
  pass
415
458
 
459
+ # Create a new request scope with the modified body
460
+ async def receive():
461
+ return {"type": "http.request", "body": modified_body}
462
+
463
+ # Update request with modified receive
464
+ request._receive = receive
465
+
416
466
  # Extract session ID from request
417
- session_id = await self.http_wrapper._extract_session_id(request)
467
+ session_id = await self.http_wrapper._extract_session_id_from_body(body)
418
468
 
419
469
  if session_id:
420
470
  # Check for existing session assignment
@@ -458,6 +508,15 @@ class HttpMcpWrapper:
458
508
  # Try extracting from JSON-RPC body
459
509
  try:
460
510
  body = await request.body()
511
+ return await self._extract_session_id_from_body(body)
512
+ except Exception:
513
+ pass
514
+
515
+ return None
516
+
517
+ async def _extract_session_id_from_body(self, body: bytes) -> str:
518
+ """Extract session ID from already-read request body."""
519
+ try:
461
520
  if body:
462
521
  payload = json.loads(body.decode("utf-8"))
463
522
  if payload.get("method") == "tools/call":
@@ -636,12 +636,14 @@ IMPORTANT TOOL CALLING RULES:
636
636
  # Multi-turn conversation - use provided messages array
637
637
  messages = message.copy()
638
638
 
639
- # Ensure system prompt is prepended if not already present
640
- if not messages or messages[0].get("role") != "system":
641
- messages.insert(0, {"role": "system", "content": system_content})
642
- else:
643
- # Replace existing system message with our constructed one
644
- messages[0] = {"role": "system", "content": system_content}
639
+ # Only add/update system message if we have non-empty content
640
+ # (Claude API rejects empty system messages - though decorator provides default)
641
+ if system_content:
642
+ if not messages or messages[0].get("role") != "system":
643
+ messages.insert(0, {"role": "system", "content": system_content})
644
+ else:
645
+ # Replace existing system message with our constructed one
646
+ messages[0] = {"role": "system", "content": system_content}
645
647
 
646
648
  # Log conversation history
647
649
  logger.info(
@@ -649,10 +651,17 @@ IMPORTANT TOOL CALLING RULES:
649
651
  )
650
652
  else:
651
653
  # Single-turn - build messages array from string
652
- messages = [
653
- {"role": "system", "content": system_content},
654
- {"role": "user", "content": message},
655
- ]
654
+ # Only include system message if non-empty (Claude API rejects empty system messages)
655
+ if system_content:
656
+ messages = [
657
+ {"role": "system", "content": system_content},
658
+ {"role": "user", "content": message},
659
+ ]
660
+ else:
661
+ # Fallback for edge case where system_content is explicitly empty
662
+ messages = [
663
+ {"role": "user", "content": message},
664
+ ]
656
665
 
657
666
  logger.info(f"🚀 Starting agentic loop for message: {message[:100]}...")
658
667
 
@@ -708,6 +717,16 @@ IMPORTANT TOOL CALLING RULES:
708
717
  if self.model:
709
718
  model_params["model"] = self.model
710
719
 
720
+ # Issue #459: Include output_schema for provider to apply vendor-specific handling
721
+ # (e.g., OpenAI needs response_format, not prompt-based JSON instructions)
722
+ if self.output_type is not str and hasattr(
723
+ self.output_type, "model_json_schema"
724
+ ):
725
+ model_params["output_schema"] = (
726
+ self.output_type.model_json_schema()
727
+ )
728
+ model_params["output_type_name"] = self.output_type.__name__
729
+
711
730
  logger.debug(
712
731
  f"📤 Delegating to mesh provider with handler-prepared params: "
713
732
  f"keys={list(model_params.keys())}"
@@ -161,36 +161,70 @@ class MeshLlmAgentInjector(BaseInjector):
161
161
  # Create UnifiedMCPProxy for the provider
162
162
  provider_proxy = self._create_provider_proxy(provider_data)
163
163
 
164
- # Update llm_agents data with provider_proxy and vendor (Phase 2)
165
- if function_id in self._llm_agents:
166
- self._llm_agents[function_id]["provider_proxy"] = provider_proxy
164
+ # Update only provider-related fields, preserving tool data if already set.
165
+ # This avoids race conditions where provider and tools updates can arrive in any order.
166
+ if function_id not in self._llm_agents:
167
+ self._llm_agents[function_id] = {}
167
168
 
168
- # Phase 2: Extract vendor from provider_data for handler selection
169
- vendor = provider_data.get("vendor", "unknown")
170
- self._llm_agents[function_id]["vendor"] = vendor
169
+ # Phase 2: Extract vendor from provider_data for handler selection
170
+ vendor = provider_data.get("vendor", "unknown")
171
171
 
172
+ self._llm_agents[function_id]["provider_proxy"] = provider_proxy
173
+ self._llm_agents[function_id]["vendor"] = vendor
174
+
175
+ logger.info(
176
+ f"✅ Set provider proxy for '{function_id}': {provider_proxy.function_name} at {provider_proxy.endpoint} (vendor={vendor})"
177
+ )
178
+
179
+ # Re-create and update MeshLlmAgent with new provider
180
+ # Get the function wrapper and metadata from DecoratorRegistry
181
+ llm_agents = DecoratorRegistry.get_mesh_llm_agents()
182
+ wrapper = None
183
+ llm_metadata = None
184
+ for agent_func_id, metadata in llm_agents.items():
185
+ if metadata.function_id == function_id:
186
+ wrapper = metadata.function
187
+ llm_metadata = metadata
188
+ break
189
+
190
+ # Check if tools are required (filter is specified)
191
+ has_filter = False
192
+ if llm_metadata and llm_metadata.config:
193
+ filter_config = llm_metadata.config.get("filter")
194
+ has_filter = filter_config is not None and len(filter_config) > 0
195
+
196
+ # If no filter specified, initialize empty tools data so we can create LLM agent without tools
197
+ # This supports simple LLM calls (text generation) that don't need tool calling
198
+ if not has_filter and "tools_metadata" not in self._llm_agents[function_id]:
199
+ self._llm_agents[function_id].update(
200
+ {
201
+ "config": llm_metadata.config if llm_metadata else {},
202
+ "output_type": llm_metadata.output_type if llm_metadata else None,
203
+ "param_name": llm_metadata.param_name if llm_metadata else "llm",
204
+ "tools_metadata": [], # No tools for simple LLM calls
205
+ "tools_proxies": {}, # No tool proxies needed
206
+ "function": llm_metadata.function if llm_metadata else None,
207
+ }
208
+ )
172
209
  logger.info(
173
- f"✅ Set provider proxy for '{function_id}': {provider_proxy.function_name} at {provider_proxy.endpoint} (vendor={vendor})"
210
+ f"✅ Initialized empty tools for '{function_id}' (no filter specified - simple LLM mode)"
174
211
  )
175
212
 
176
- # Re-create and update MeshLlmAgent with new provider
177
- # Get the function wrapper from DecoratorRegistry
178
- llm_agents = DecoratorRegistry.get_mesh_llm_agents()
179
- wrapper = None
180
- for agent_func_id, metadata in llm_agents.items():
181
- if metadata.function_id == function_id:
182
- wrapper = metadata.function
183
- break
184
-
185
- if wrapper and hasattr(wrapper, "_mesh_update_llm_agent"):
186
- llm_agent = self._create_llm_agent(function_id)
187
- wrapper._mesh_update_llm_agent(llm_agent)
188
- logger.info(
189
- f"🔄 Updated wrapper with new MeshLlmAgent (with provider) for '{function_id}'"
190
- )
191
- else:
192
- logger.warning(
193
- f"⚠️ Function '{function_id}' not found in _llm_agents, cannot set provider proxy"
213
+ # Update wrapper if we have tools data (either from filter matching or initialized empty)
214
+ if (
215
+ wrapper
216
+ and hasattr(wrapper, "_mesh_update_llm_agent")
217
+ and "tools_metadata" in self._llm_agents[function_id]
218
+ ):
219
+ llm_agent = self._create_llm_agent(function_id)
220
+ wrapper._mesh_update_llm_agent(llm_agent)
221
+ logger.info(
222
+ f"🔄 Updated wrapper with MeshLlmAgent for '{function_id}'"
223
+ + (" (with tools)" if has_filter else " (simple LLM mode)")
224
+ )
225
+ elif wrapper and hasattr(wrapper, "_mesh_update_llm_agent") and has_filter:
226
+ logger.debug(
227
+ f"⏳ Provider set for '{function_id}', waiting for tools before updating wrapper"
194
228
  )
195
229
 
196
230
  def _create_provider_proxy(self, provider_data: dict[str, Any]) -> UnifiedMCPProxy:
@@ -273,21 +307,23 @@ class MeshLlmAgentInjector(BaseInjector):
273
307
  logger.error(f"❌ Error creating proxy for tool {tool_name}: {e}")
274
308
  # Continue processing other tools
275
309
 
276
- # Provider proxy will be set separately via process_llm_providers()
277
- # (v0.6.1 - providers come from llm_providers field, not dependencies)
278
- provider_proxy = None
279
-
280
- # Store LLM agent data with both metadata and proxies
281
- # Keep original tool metadata for schema building
282
- self._llm_agents[function_id] = {
283
- "config": llm_metadata.config,
284
- "output_type": llm_metadata.output_type,
285
- "param_name": llm_metadata.param_name,
286
- "tools_metadata": tools, # Original metadata for schema building
287
- "tools_proxies": tool_proxies_map, # Proxies for execution
288
- "function": llm_metadata.function,
289
- "provider_proxy": provider_proxy, # Provider proxy for mesh delegation
290
- }
310
+ # Update only tool-related fields, preserving provider_proxy if already set.
311
+ # Provider proxy is managed separately by process_llm_providers().
312
+ # This avoids race conditions where tools update wipes out provider resolution.
313
+ if function_id not in self._llm_agents:
314
+ self._llm_agents[function_id] = {}
315
+
316
+ self._llm_agents[function_id].update(
317
+ {
318
+ "config": llm_metadata.config,
319
+ "output_type": llm_metadata.output_type,
320
+ "param_name": llm_metadata.param_name,
321
+ "tools_metadata": tools, # Original metadata for schema building
322
+ "tools_proxies": tool_proxies_map, # Proxies for execution
323
+ "function": llm_metadata.function,
324
+ # Note: provider_proxy is NOT set here - managed by _process_function_provider
325
+ }
326
+ )
291
327
 
292
328
  logger.info(
293
329
  f"✅ Processed {len(tool_proxies_map)} tools for LLM function '{function_id}'"
@@ -380,7 +416,7 @@ class MeshLlmAgentInjector(BaseInjector):
380
416
  """
381
417
  Create wrapper that injects MeshLlmAgent into function parameters.
382
418
 
383
- Like McpMeshAgent injection, this creates a wrapper at decorator time with llm_agent=None,
419
+ Like McpMeshTool injection, this creates a wrapper at decorator time with llm_agent=None,
384
420
  which gets updated during heartbeat when tools arrive from registry.
385
421
 
386
422
  Args:
@@ -5,15 +5,28 @@ This package provides vendor-specific customization for different LLM providers
5
5
  (Claude, OpenAI, Gemini, etc.) to optimize API calls and response handling.
6
6
  """
7
7
 
8
- from .base_provider_handler import BaseProviderHandler
8
+ from .base_provider_handler import (
9
+ BASE_TOOL_INSTRUCTIONS,
10
+ CLAUDE_ANTI_XML_INSTRUCTION,
11
+ BaseProviderHandler,
12
+ make_schema_strict,
13
+ )
9
14
  from .claude_handler import ClaudeHandler
15
+ from .gemini_handler import GeminiHandler
10
16
  from .generic_handler import GenericHandler
11
17
  from .openai_handler import OpenAIHandler
12
18
  from .provider_handler_registry import ProviderHandlerRegistry
13
19
 
14
20
  __all__ = [
21
+ # Constants
22
+ "BASE_TOOL_INSTRUCTIONS",
23
+ "CLAUDE_ANTI_XML_INSTRUCTION",
24
+ # Utilities
25
+ "make_schema_strict",
26
+ # Handlers
15
27
  "BaseProviderHandler",
16
28
  "ClaudeHandler",
29
+ "GeminiHandler",
17
30
  "OpenAIHandler",
18
31
  "GenericHandler",
19
32
  "ProviderHandlerRegistry",
@@ -5,11 +5,117 @@ This module defines the abstract base class for provider-specific handlers
5
5
  that customize how different LLM vendors (Claude, OpenAI, Gemini, etc.) are called.
6
6
  """
7
7
 
8
+ import copy
8
9
  from abc import ABC, abstractmethod
9
- from typing import Any, Dict, List, Optional
10
+ from typing import Any, Optional
10
11
 
11
12
  from pydantic import BaseModel
12
13
 
14
+ # ============================================================================
15
+ # Shared Constants
16
+ # ============================================================================
17
+
18
+ # Base tool calling instructions shared across all providers.
19
+ # Claude handler adds anti-XML instruction on top of this.
20
+ BASE_TOOL_INSTRUCTIONS = """
21
+ IMPORTANT TOOL CALLING RULES:
22
+ - You have access to tools that you can call to gather information
23
+ - Make ONE tool call at a time
24
+ - After receiving tool results, you can make additional calls if needed
25
+ - Once you have all needed information, provide your final response
26
+ """
27
+
28
+ # Anti-XML instruction for Claude (prevents <invoke> style tool calls).
29
+ CLAUDE_ANTI_XML_INSTRUCTION = (
30
+ '- NEVER use XML-style syntax like <invoke name="tool_name"/>'
31
+ )
32
+
33
+
34
+ # ============================================================================
35
+ # Shared Schema Utilities
36
+ # ============================================================================
37
+
38
+
39
+ def make_schema_strict(
40
+ schema: dict[str, Any],
41
+ add_all_required: bool = True,
42
+ ) -> dict[str, Any]:
43
+ """
44
+ Make a JSON schema strict for structured output.
45
+
46
+ This is a shared utility used by OpenAI, Gemini, and Claude handlers.
47
+ Adds additionalProperties: false to all object types and optionally
48
+ ensures 'required' includes all property keys.
49
+
50
+ Args:
51
+ schema: JSON schema to make strict
52
+ add_all_required: If True, set 'required' to include ALL property keys.
53
+ OpenAI and Gemini require this; Claude does not.
54
+ Default: True
55
+
56
+ Returns:
57
+ New schema with strict constraints (original not mutated)
58
+ """
59
+ result = copy.deepcopy(schema)
60
+ _add_strict_constraints_recursive(result, add_all_required)
61
+ return result
62
+
63
+
64
+ def _add_strict_constraints_recursive(obj: Any, add_all_required: bool) -> None:
65
+ """
66
+ Recursively add strict constraints to a schema object.
67
+
68
+ Args:
69
+ obj: Schema object to process (mutated in place)
70
+ add_all_required: Whether to set required to all property keys
71
+ """
72
+ if not isinstance(obj, dict):
73
+ return
74
+
75
+ # If this is an object type, add additionalProperties: false
76
+ if obj.get("type") == "object":
77
+ obj["additionalProperties"] = False
78
+
79
+ # Optionally set required to include all property keys
80
+ if add_all_required and "properties" in obj:
81
+ obj["required"] = list(obj["properties"].keys())
82
+
83
+ # Process $defs (Pydantic uses this for nested models)
84
+ if "$defs" in obj:
85
+ for def_schema in obj["$defs"].values():
86
+ _add_strict_constraints_recursive(def_schema, add_all_required)
87
+
88
+ # Process properties
89
+ if "properties" in obj:
90
+ for prop_schema in obj["properties"].values():
91
+ _add_strict_constraints_recursive(prop_schema, add_all_required)
92
+
93
+ # Process items (for arrays)
94
+ # items can be an object (single schema) or a list (tuple validation in older drafts)
95
+ if "items" in obj:
96
+ items = obj["items"]
97
+ if isinstance(items, dict):
98
+ _add_strict_constraints_recursive(items, add_all_required)
99
+ elif isinstance(items, list):
100
+ for item in items:
101
+ _add_strict_constraints_recursive(item, add_all_required)
102
+
103
+ # Process prefixItems (tuple validation in JSON Schema draft 2020-12)
104
+ if "prefixItems" in obj:
105
+ for item in obj["prefixItems"]:
106
+ _add_strict_constraints_recursive(item, add_all_required)
107
+
108
+ # Process anyOf, oneOf, allOf
109
+ for key in ("anyOf", "oneOf", "allOf"):
110
+ if key in obj:
111
+ for item in obj[key]:
112
+ _add_strict_constraints_recursive(item, add_all_required)
113
+
114
+
115
+ # ============================================================================
116
+ # Base Provider Handler
117
+ # ============================================================================
118
+
13
119
 
14
120
  class BaseProviderHandler(ABC):
15
121
  """
@@ -43,11 +149,11 @@ class BaseProviderHandler(ABC):
43
149
  @abstractmethod
44
150
  def prepare_request(
45
151
  self,
46
- messages: List[Dict[str, Any]],
47
- tools: Optional[List[Dict[str, Any]]],
152
+ messages: list[dict[str, Any]],
153
+ tools: Optional[list[dict[str, Any]]],
48
154
  output_type: type[BaseModel],
49
- **kwargs: Any
50
- ) -> Dict[str, Any]:
155
+ **kwargs: Any,
156
+ ) -> dict[str, Any]:
51
157
  """
52
158
  Prepare vendor-specific request parameters.
53
159
 
@@ -74,8 +180,8 @@ class BaseProviderHandler(ABC):
74
180
  def format_system_prompt(
75
181
  self,
76
182
  base_prompt: str,
77
- tool_schemas: Optional[List[Dict[str, Any]]],
78
- output_type: type[BaseModel]
183
+ tool_schemas: Optional[list[dict[str, Any]]],
184
+ output_type: type[BaseModel],
79
185
  ) -> str:
80
186
  """
81
187
  Format system prompt for vendor-specific requirements.
@@ -95,7 +201,7 @@ class BaseProviderHandler(ABC):
95
201
  """
96
202
  pass
97
203
 
98
- def get_vendor_capabilities(self) -> Dict[str, bool]:
204
+ def get_vendor_capabilities(self) -> dict[str, bool]:
99
205
  """
100
206
  Return vendor-specific capability flags.
101
207