mcp-mesh 0.8.0b9__tar.gz → 0.8.1__tar.gz

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 (86) hide show
  1. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/.gitignore +9 -0
  2. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/PKG-INFO +4 -2
  3. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/__init__.py +1 -1
  4. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/dependency_injector.py +9 -0
  5. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/mesh_llm_agent.py +36 -14
  6. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/mesh_llm_agent_injector.py +189 -35
  7. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +5 -1
  8. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +12 -1
  9. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/mesh/decorators.py +174 -92
  10. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/mesh/helpers.py +52 -0
  11. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/pyproject.toml +7 -5
  12. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/LICENSE +0 -0
  13. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/README.md +0 -0
  14. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/__init__.py +0 -0
  15. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/async_mcp_client.py +0 -0
  16. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/base_injector.py +0 -0
  17. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/decorator_registry.py +0 -0
  18. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/http_wrapper.py +0 -0
  19. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/llm_config.py +0 -0
  20. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/llm_errors.py +0 -0
  21. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/__init__.py +0 -0
  22. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/base_provider_handler.py +0 -0
  23. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/claude_handler.py +0 -0
  24. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/gemini_handler.py +0 -0
  25. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/generic_handler.py +0 -0
  26. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/openai_handler.py +0 -0
  27. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/provider_handler_registry.py +0 -0
  28. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/response_parser.py +0 -0
  29. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/self_dependency_proxy.py +0 -0
  30. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/session_aware_client.py +0 -0
  31. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/session_manager.py +0 -0
  32. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/signature_analyzer.py +0 -0
  33. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/tool_executor.py +0 -0
  34. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/tool_schema_builder.py +0 -0
  35. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/engine/unified_mcp_proxy.py +0 -0
  36. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/__init__.py +0 -0
  37. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_heartbeat/__init__.py +0 -0
  38. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +0 -0
  39. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/__init__.py +0 -0
  40. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/api_pipeline.py +0 -0
  41. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/api_server_setup.py +0 -0
  42. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/fastapi_discovery.py +0 -0
  43. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/middleware_integration.py +0 -0
  44. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/route_collection.py +0 -0
  45. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/route_integration.py +0 -0
  46. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_heartbeat/__init__.py +0 -0
  47. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/__init__.py +0 -0
  48. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/configuration.py +0 -0
  49. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/decorator_collection.py +0 -0
  50. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +0 -0
  51. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +0 -0
  52. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +0 -0
  53. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +0 -0
  54. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/lifespan_factory.py +0 -0
  55. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/server_discovery.py +0 -0
  56. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +0 -0
  57. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +0 -0
  58. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/shared/__init__.py +0 -0
  59. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/shared/base_step.py +0 -0
  60. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -0
  61. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/shared/pipeline_types.py +0 -0
  62. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/reload.py +0 -0
  63. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/reload_runner.py +0 -0
  64. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/__init__.py +0 -0
  65. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/config_resolver.py +0 -0
  66. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/content_extractor.py +0 -0
  67. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/defaults.py +0 -0
  68. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/fast_heartbeat_status.py +0 -0
  69. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/fastapi_middleware_manager.py +0 -0
  70. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/health_check_manager.py +0 -0
  71. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/host_resolver.py +0 -0
  72. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/logging_config.py +0 -0
  73. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/server_discovery.py +0 -0
  74. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/simple_shutdown.py +0 -0
  75. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/sse_parser.py +0 -0
  76. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/shared/support_types.py +0 -0
  77. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/agent_context_helper.py +0 -0
  78. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/context.py +0 -0
  79. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/execution_tracer.py +0 -0
  80. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/fastapi_tracing_middleware.py +0 -0
  81. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/redis_metadata_publisher.py +0 -0
  82. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/trace_context_helper.py +0 -0
  83. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/utils.py +0 -0
  84. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/_mcp_mesh/utils/fastmcp_schema_extractor.py +0 -0
  85. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/mesh/__init__.py +0 -0
  86. {mcp_mesh-0.8.0b9 → mcp_mesh-0.8.1}/mesh/types.py +0 -0
@@ -256,3 +256,12 @@ src/runtime/core/index.js
256
256
  src/runtime/core/index.d.ts
257
257
  test/
258
258
  scaffold-test/
259
+
260
+ # Test suite build outputs
261
+ tests/src-tests/out/
262
+ tests/lib-tests/out/
263
+
264
+ # Allow test suite build artifacts
265
+ !tests/**/build/
266
+ .claude/
267
+ *.org
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-mesh
3
- Version: 0.8.0b9
3
+ Version: 0.8.1
4
4
  Summary: Kubernetes-native platform for distributed MCP applications
5
5
  Project-URL: Homepage, https://github.com/dhyansraj/mcp-mesh
6
6
  Project-URL: Documentation, https://github.com/dhyansraj/mcp-mesh/tree/main/docs
@@ -18,6 +18,8 @@ Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
21
23
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
25
  Classifier: Topic :: System :: Distributed Computing
@@ -30,7 +32,7 @@ Requires-Dist: fastmcp<3.0.0,>=2.8.0
30
32
  Requires-Dist: httpx<1.0.0,>=0.25.0
31
33
  Requires-Dist: jinja2>=3.1.0
32
34
  Requires-Dist: litellm>=1.30.0
33
- Requires-Dist: mcp-mesh-core>=0.8.0b9
35
+ Requires-Dist: mcp-mesh-core>=0.8.1
34
36
  Requires-Dist: mcp<2.0.0,>=1.9.0
35
37
  Requires-Dist: prometheus-client<1.0.0,>=0.19.0
36
38
  Requires-Dist: pydantic<3.0.0,>=2.4.0
@@ -31,7 +31,7 @@ from .engine.decorator_registry import (
31
31
  get_decorator_stats,
32
32
  )
33
33
 
34
- __version__ = "0.8.0b9"
34
+ __version__ = "0.8.1"
35
35
 
36
36
  # Store reference to runtime processor if initialized
37
37
  _runtime_processor = None
@@ -348,6 +348,15 @@ class DependencyInjector:
348
348
  logger.debug(f"🤖 Creating LLM injection wrapper for {function_id}")
349
349
  return self._llm_injector.create_injection_wrapper(func, function_id)
350
350
 
351
+ def initialize_direct_llm_agents(self) -> None:
352
+ """
353
+ Initialize LLM agents that use direct LiteLLM (no mesh delegation).
354
+
355
+ This should be called during agent startup to initialize agents that
356
+ don't need to wait for registry response.
357
+ """
358
+ self._llm_injector.initialize_direct_llm_agents()
359
+
351
360
  def create_injection_wrapper(
352
361
  self, func: Callable, dependencies: list[str]
353
362
  ) -> Callable:
@@ -14,8 +14,12 @@ from typing import Any, Dict, List, Literal, Optional, Union
14
14
  from pydantic import BaseModel
15
15
 
16
16
  from .llm_config import LLMConfig
17
- from .llm_errors import (LLMAPIError, MaxIterationsError, ResponseParseError,
18
- ToolExecutionError)
17
+ from .llm_errors import (
18
+ LLMAPIError,
19
+ MaxIterationsError,
20
+ ResponseParseError,
21
+ ToolExecutionError,
22
+ )
19
23
  from .provider_handlers import ProviderHandlerRegistry
20
24
  from .response_parser import ResponseParser
21
25
  from .tool_executor import ToolExecutor
@@ -23,8 +27,7 @@ from .tool_schema_builder import ToolSchemaBuilder
23
27
 
24
28
  # Import Jinja2 for template rendering
25
29
  try:
26
- from jinja2 import (Environment, FileSystemLoader, Template,
27
- TemplateSyntaxError)
30
+ from jinja2 import Environment, FileSystemLoader, Template, TemplateSyntaxError
28
31
  except ImportError:
29
32
  Environment = None
30
33
  FileSystemLoader = None
@@ -633,12 +636,14 @@ IMPORTANT TOOL CALLING RULES:
633
636
  # Multi-turn conversation - use provided messages array
634
637
  messages = message.copy()
635
638
 
636
- # Ensure system prompt is prepended if not already present
637
- if not messages or messages[0].get("role") != "system":
638
- messages.insert(0, {"role": "system", "content": system_content})
639
- else:
640
- # Replace existing system message with our constructed one
641
- 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}
642
647
 
643
648
  # Log conversation history
644
649
  logger.info(
@@ -646,10 +651,17 @@ IMPORTANT TOOL CALLING RULES:
646
651
  )
647
652
  else:
648
653
  # Single-turn - build messages array from string
649
- messages = [
650
- {"role": "system", "content": system_content},
651
- {"role": "user", "content": message},
652
- ]
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
+ ]
653
665
 
654
666
  logger.info(f"🚀 Starting agentic loop for message: {message[:100]}...")
655
667
 
@@ -705,6 +717,16 @@ IMPORTANT TOOL CALLING RULES:
705
717
  if self.model:
706
718
  model_params["model"] = self.model
707
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
+
708
730
  logger.debug(
709
731
  f"📤 Delegating to mesh provider with handler-prepared params: "
710
732
  f"keys={list(model_params.keys())}"
@@ -65,6 +65,75 @@ class MeshLlmAgentInjector(BaseInjector):
65
65
  super().__init__()
66
66
  self._llm_agents: dict[str, dict[str, Any]] = {}
67
67
 
68
+ def initialize_direct_llm_agents(self) -> None:
69
+ """
70
+ Initialize LLM agents that use direct LiteLLM (no mesh delegation).
71
+
72
+ This handles the case where:
73
+ - provider is a string (e.g., "claude") - direct LiteLLM call
74
+ - filter is None or empty - no mesh tools needed
75
+
76
+ These agents don't need to wait for registry response since all
77
+ information is available at decorator time.
78
+ """
79
+ llm_agents = DecoratorRegistry.get_mesh_llm_agents()
80
+
81
+ for function_id, llm_metadata in llm_agents.items():
82
+ config = llm_metadata.config
83
+ provider = config.get("provider")
84
+ filter_config = config.get("filter")
85
+
86
+ # Check if this is a direct LiteLLM agent (provider is string, not dict)
87
+ is_direct_llm = isinstance(provider, str)
88
+
89
+ # Check if no tools needed (filter is None or empty)
90
+ has_no_filter = filter_config is None or (
91
+ isinstance(filter_config, list) and len(filter_config) == 0
92
+ )
93
+
94
+ if is_direct_llm and has_no_filter:
95
+ # Skip if already initialized
96
+ if function_id in self._llm_agents:
97
+ continue
98
+
99
+ logger.info(
100
+ f"🔧 Initializing direct LiteLLM agent for '{function_id}' "
101
+ f"(provider={provider}, no filter)"
102
+ )
103
+
104
+ # Initialize empty tools data for direct LiteLLM
105
+ self._llm_agents[function_id] = {
106
+ "config": config,
107
+ "output_type": llm_metadata.output_type,
108
+ "param_name": llm_metadata.param_name,
109
+ "tools_metadata": [], # No tools for direct LiteLLM
110
+ "tools_proxies": {}, # No tool proxies needed
111
+ "function": llm_metadata.function,
112
+ "provider_proxy": None, # No mesh delegation
113
+ }
114
+
115
+ # Get the wrapper and update it with LLM agent
116
+ wrapper = llm_metadata.function
117
+ if wrapper and hasattr(wrapper, "_mesh_update_llm_agent"):
118
+ llm_agent = self._create_llm_agent(function_id)
119
+ wrapper._mesh_update_llm_agent(llm_agent)
120
+ logger.info(
121
+ f"🔄 Updated wrapper with MeshLlmAgent for '{function_id}' (direct LiteLLM mode)"
122
+ )
123
+
124
+ # Set factory for per-call context agent creation (template support)
125
+ if config.get("is_template", False):
126
+ def create_context_agent(
127
+ context_value: Any, _func_id: str = function_id
128
+ ) -> MeshLlmAgent:
129
+ """Factory to create MeshLlmAgent with context for template rendering."""
130
+ return self._create_llm_agent(_func_id, context_value=context_value)
131
+
132
+ wrapper._mesh_create_context_agent = create_context_agent
133
+ logger.info(
134
+ f"🎯 Set context agent factory for template-based function '{function_id}' (direct LiteLLM mode)"
135
+ )
136
+
68
137
  def _build_function_name_to_id_mapping(self) -> dict[str, str]:
69
138
  """
70
139
  Build mapping from function_name to function_id.
@@ -176,16 +245,41 @@ class MeshLlmAgentInjector(BaseInjector):
176
245
  f"✅ Set provider proxy for '{function_id}': {provider_proxy.function_name} at {provider_proxy.endpoint} (vendor={vendor})"
177
246
  )
178
247
 
179
- # Re-create and update MeshLlmAgent with new provider (only if tools are also available)
180
- # Get the function wrapper from DecoratorRegistry
248
+ # Re-create and update MeshLlmAgent with new provider
249
+ # Get the function wrapper and metadata from DecoratorRegistry
181
250
  llm_agents = DecoratorRegistry.get_mesh_llm_agents()
182
251
  wrapper = None
252
+ llm_metadata = None
183
253
  for agent_func_id, metadata in llm_agents.items():
184
254
  if metadata.function_id == function_id:
185
255
  wrapper = metadata.function
256
+ llm_metadata = metadata
186
257
  break
187
258
 
188
- # Only update wrapper if we have both tools and provider (tools_metadata indicates tools were processed)
259
+ # Check if tools are required (filter is specified)
260
+ has_filter = False
261
+ if llm_metadata and llm_metadata.config:
262
+ filter_config = llm_metadata.config.get("filter")
263
+ has_filter = filter_config is not None and len(filter_config) > 0
264
+
265
+ # If no filter specified, initialize empty tools data so we can create LLM agent without tools
266
+ # This supports simple LLM calls (text generation) that don't need tool calling
267
+ if not has_filter and "tools_metadata" not in self._llm_agents[function_id]:
268
+ self._llm_agents[function_id].update(
269
+ {
270
+ "config": llm_metadata.config if llm_metadata else {},
271
+ "output_type": llm_metadata.output_type if llm_metadata else None,
272
+ "param_name": llm_metadata.param_name if llm_metadata else "llm",
273
+ "tools_metadata": [], # No tools for simple LLM calls
274
+ "tools_proxies": {}, # No tool proxies needed
275
+ "function": llm_metadata.function if llm_metadata else None,
276
+ }
277
+ )
278
+ logger.info(
279
+ f"✅ Initialized empty tools for '{function_id}' (no filter specified - simple LLM mode)"
280
+ )
281
+
282
+ # Update wrapper if we have tools data (either from filter matching or initialized empty)
189
283
  if (
190
284
  wrapper
191
285
  and hasattr(wrapper, "_mesh_update_llm_agent")
@@ -194,9 +288,26 @@ class MeshLlmAgentInjector(BaseInjector):
194
288
  llm_agent = self._create_llm_agent(function_id)
195
289
  wrapper._mesh_update_llm_agent(llm_agent)
196
290
  logger.info(
197
- f"🔄 Updated wrapper with new MeshLlmAgent (with provider) for '{function_id}'"
291
+ f"🔄 Updated wrapper with MeshLlmAgent for '{function_id}'"
292
+ + (" (with tools)" if has_filter else " (simple LLM mode)")
198
293
  )
199
- elif wrapper and hasattr(wrapper, "_mesh_update_llm_agent"):
294
+
295
+ # Set factory for per-call context agent creation (template support)
296
+ # This is critical for filter=None cases where _process_function_tools isn't called
297
+ config_dict = llm_metadata.config if llm_metadata else {}
298
+ if config_dict.get("is_template", False):
299
+ # Capture function_id by value using default argument to avoid closure issues
300
+ def create_context_agent(
301
+ context_value: Any, _func_id: str = function_id
302
+ ) -> MeshLlmAgent:
303
+ """Factory to create MeshLlmAgent with context for template rendering."""
304
+ return self._create_llm_agent(_func_id, context_value=context_value)
305
+
306
+ wrapper._mesh_create_context_agent = create_context_agent
307
+ logger.info(
308
+ f"🎯 Set context agent factory for template-based function '{function_id}' (simple LLM mode)"
309
+ )
310
+ elif wrapper and hasattr(wrapper, "_mesh_update_llm_agent") and has_filter:
200
311
  logger.debug(
201
312
  f"⏳ Provider set for '{function_id}', waiting for tools before updating wrapper"
202
313
  )
@@ -432,36 +543,60 @@ class MeshLlmAgentInjector(BaseInjector):
432
543
  def inject_llm_agent(func: Callable, args: tuple, kwargs: dict) -> tuple:
433
544
  """Inject LLM agent into kwargs if not provided."""
434
545
  if param_name not in kwargs or kwargs.get(param_name) is None:
435
- # Phase 4: Check if templates are enabled
546
+ # Get config from runtime data or fallback to decorator registry.
547
+ # Runtime data (self._llm_agents) is populated during heartbeat and has
548
+ # tools/provider info. Decorator registry is populated at decorator time
549
+ # and always has config/context_param. For self-dependency calls that
550
+ # happen before heartbeat, we need the decorator registry fallback.
551
+ agent_data = None
552
+ config_dict = None
553
+
554
+ # Try runtime data first (has tools, provider from heartbeat)
436
555
  if function_id in self._llm_agents:
437
556
  agent_data = self._llm_agents[function_id]
438
- config_dict = agent_data["config"]
439
- is_template = config_dict.get("is_template", False)
440
-
441
- if is_template:
442
- # Templates enabled - create per-call agent with context
443
- # Import signature analyzer for context detection
444
- from .signature_analyzer import get_context_parameter_name
445
-
446
- # Detect context parameter
447
- context_param_name = config_dict.get("context_param")
448
- context_info = get_context_parameter_name(
449
- func, explicit_name=context_param_name
557
+ config_dict = agent_data.get("config")
558
+
559
+ # Fallback to decorator registry (always available, has context_param)
560
+ # This is critical for self-dependency calls that happen before heartbeat
561
+ if config_dict is None:
562
+ llm_agents_registry = DecoratorRegistry.get_mesh_llm_agents()
563
+ if function_id in llm_agents_registry:
564
+ llm_metadata = llm_agents_registry[function_id]
565
+ config_dict = llm_metadata.config
566
+ logger.debug(
567
+ f"🔄 Using DecoratorRegistry fallback for '{function_id}' config (self-dependency before heartbeat)"
450
568
  )
451
569
 
452
- # Extract context value from call
453
- context_value = None
454
- if context_info is not None:
455
- ctx_name, ctx_index = context_info
570
+ # Check if templates are enabled
571
+ is_template = config_dict.get("is_template", False) if config_dict else False
572
+
573
+ if is_template and config_dict:
574
+ # Templates enabled - create per-call agent with context
575
+ # Import signature analyzer for context detection
576
+ from .signature_analyzer import get_context_parameter_name
456
577
 
457
- # Try kwargs first
458
- if ctx_name in kwargs:
459
- context_value = kwargs[ctx_name]
460
- # Then try positional args
461
- elif ctx_index < len(args):
462
- context_value = args[ctx_index]
578
+ # Detect context parameter
579
+ context_param_name = config_dict.get("context_param")
580
+ context_info = get_context_parameter_name(
581
+ func, explicit_name=context_param_name
582
+ )
463
583
 
464
- # Create agent with context for this call
584
+ # Extract context value from call
585
+ context_value = None
586
+ if context_info is not None:
587
+ ctx_name, ctx_index = context_info
588
+
589
+ # Try kwargs first
590
+ if ctx_name in kwargs:
591
+ context_value = kwargs[ctx_name]
592
+ # Then try positional args
593
+ elif ctx_index < len(args):
594
+ context_value = args[ctx_index]
595
+
596
+ # Create agent with context for this call
597
+ # Note: _create_llm_agent requires function_id in self._llm_agents
598
+ # If not available yet, use cached agent with context_value set directly
599
+ if function_id in self._llm_agents:
465
600
  current_agent = self._create_llm_agent(
466
601
  function_id, context_value=context_value
467
602
  )
@@ -469,22 +604,41 @@ class MeshLlmAgentInjector(BaseInjector):
469
604
  f"🤖 Created MeshLlmAgent with context for {func.__name__}.{param_name}"
470
605
  )
471
606
  else:
472
- # No template - use cached agent (existing behavior)
607
+ # Runtime data not yet available - use cached agent but log warning
608
+ # The cached agent may have been created without context
473
609
  current_agent = wrapper._mesh_llm_agent
474
610
  if current_agent is not None:
475
- logger.debug(
476
- f"🤖 Injected MeshLlmAgent into {func.__name__}.{param_name}"
477
- )
611
+ # Update context on the cached agent if possible
612
+ if hasattr(current_agent, "_context_value"):
613
+ current_agent._context_value = context_value
614
+ logger.debug(
615
+ f"🤖 Updated context on cached MeshLlmAgent for {func.__name__}.{param_name}"
616
+ )
617
+ else:
618
+ logger.debug(
619
+ f"🤖 Injected cached MeshLlmAgent into {func.__name__}.{param_name} (context may not be applied)"
620
+ )
478
621
  else:
479
622
  logger.warning(
480
623
  f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (tools not yet received from registry)"
481
624
  )
625
+ elif config_dict:
626
+ # No template - use cached agent (existing behavior)
627
+ current_agent = wrapper._mesh_llm_agent
628
+ if current_agent is not None:
629
+ logger.debug(
630
+ f"🤖 Injected MeshLlmAgent into {func.__name__}.{param_name}"
631
+ )
632
+ else:
633
+ logger.warning(
634
+ f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (tools not yet received from registry)"
635
+ )
482
636
  else:
483
- # No agent data - use cached (backward compatibility)
637
+ # No config found anywhere - use cached (backward compatibility)
484
638
  current_agent = wrapper._mesh_llm_agent
485
639
  if current_agent is None:
486
640
  logger.warning(
487
- f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (tools not yet received from registry)"
641
+ f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (no config found)"
488
642
  )
489
643
 
490
644
  kwargs[param_name] = current_agent
@@ -104,9 +104,10 @@ def _build_api_agent_spec(context: dict[str, Any], service_id: str = None) -> An
104
104
  # Build dependency specs
105
105
  deps = []
106
106
  for dep_cap in dependencies:
107
+ # Tags must be serialized to JSON string (Rust core expects string, not list)
107
108
  dep_spec = core.DependencySpec(
108
109
  capability=dep_cap,
109
- tags=[],
110
+ tags=json.dumps([]),
110
111
  version=None,
111
112
  )
112
113
  deps.append(dep_spec)
@@ -136,6 +137,7 @@ def _build_api_agent_spec(context: dict[str, Any], service_id: str = None) -> An
136
137
  http_port=http_port,
137
138
  http_host=http_host,
138
139
  namespace=namespace,
140
+ agent_type="api", # API services only consume capabilities, not provide them
139
141
  tools=tools if tools else None,
140
142
  llm_agents=None, # API services don't have LLM agents
141
143
  heartbeat_interval=heartbeat_interval,
@@ -272,6 +274,8 @@ async def _handle_api_dependency_change(
272
274
  )
273
275
  if not current_service_id:
274
276
  # Use config resolver for consistent env var handling
277
+ from ...shared.config_resolver import get_config_value
278
+
275
279
  current_service_id = get_config_value("MCP_MESH_AGENT_ID")
276
280
 
277
281
  is_self_dependency = (
@@ -117,9 +117,12 @@ def _build_agent_spec(context: dict[str, Any]) -> Any:
117
117
  # Build dependency specs
118
118
  deps = []
119
119
  for dep_info in tool_metadata.get("dependencies", []):
120
+ # Serialize tags to JSON to support nested arrays for OR alternatives
121
+ # e.g., ["addition", ["python", "typescript"]] -> addition AND (python OR typescript)
122
+ tags_json = json.dumps(dep_info.get("tags", []))
120
123
  dep_spec = core.DependencySpec(
121
124
  capability=dep_info.get("capability", ""),
122
- tags=dep_info.get("tags", []),
125
+ tags=tags_json,
123
126
  version=dep_info.get("version"),
124
127
  )
125
128
  deps.append(dep_spec)
@@ -269,6 +272,14 @@ async def _handle_mesh_event(event: Any, context: dict[str, Any]) -> None:
269
272
  if event_type == "agent_registered":
270
273
  logger.info(f"Agent registered with ID: {event.agent_id}")
271
274
 
275
+ # Initialize direct LiteLLM agents that don't need mesh delegation
276
+ # These agents have provider="string" and filter=None, so all info is
277
+ # available at decorator time - no need to wait for registry response
278
+ from ...engine.dependency_injector import get_global_injector
279
+
280
+ injector = get_global_injector()
281
+ injector.initialize_direct_llm_agents()
282
+
272
283
  elif event_type == "registration_failed":
273
284
  logger.error(f"Agent registration failed: {event.error}")
274
285