mcp-mesh 0.5.7__py3-none-any.whl → 0.6.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 (39) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/base_injector.py +171 -0
  3. _mcp_mesh/engine/decorator_registry.py +136 -33
  4. _mcp_mesh/engine/dependency_injector.py +91 -18
  5. _mcp_mesh/engine/http_wrapper.py +5 -22
  6. _mcp_mesh/engine/llm_config.py +41 -0
  7. _mcp_mesh/engine/llm_errors.py +115 -0
  8. _mcp_mesh/engine/mesh_llm_agent.py +440 -0
  9. _mcp_mesh/engine/mesh_llm_agent_injector.py +487 -0
  10. _mcp_mesh/engine/response_parser.py +240 -0
  11. _mcp_mesh/engine/signature_analyzer.py +229 -99
  12. _mcp_mesh/engine/tool_executor.py +169 -0
  13. _mcp_mesh/engine/tool_schema_builder.py +125 -0
  14. _mcp_mesh/engine/unified_mcp_proxy.py +14 -12
  15. _mcp_mesh/generated/.openapi-generator/FILES +4 -0
  16. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +81 -44
  17. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +72 -35
  18. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +132 -0
  19. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +172 -0
  20. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +92 -0
  21. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +121 -0
  22. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +98 -51
  23. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +93 -44
  24. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +84 -41
  25. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +9 -72
  26. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +6 -3
  27. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +222 -0
  28. _mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +7 -0
  29. _mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +65 -4
  30. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +2 -2
  31. _mcp_mesh/shared/registry_client_wrapper.py +60 -4
  32. _mcp_mesh/utils/fastmcp_schema_extractor.py +476 -0
  33. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/METADATA +1 -1
  34. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/RECORD +39 -25
  35. mesh/__init__.py +8 -4
  36. mesh/decorators.py +344 -2
  37. mesh/types.py +145 -94
  38. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/WHEEL +0 -0
  39. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,487 @@
1
+ """
2
+ MeshLlmAgent dependency injection system.
3
+
4
+ Handles injection of MeshLlmAgent proxies into function parameters.
5
+ Similar to DependencyInjector but specialized for LLM agent injection.
6
+ """
7
+
8
+ import inspect
9
+ import logging
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+
13
+ from .base_injector import BaseInjector
14
+ from .decorator_registry import DecoratorRegistry
15
+ from .llm_config import LLMConfig
16
+ from .mesh_llm_agent import MeshLlmAgent
17
+ from .unified_mcp_proxy import UnifiedMCPProxy
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class MeshLlmAgentInjector(BaseInjector):
23
+ """
24
+ Manages dynamic injection of MeshLlmAgent proxies.
25
+
26
+ This class:
27
+ 1. Consumes llm_tools from registry response
28
+ 2. Creates UnifiedMCPProxy instances for each tool
29
+ 3. Creates MeshLlmAgent instances with config + proxies + output_type
30
+ 4. Injects MeshLlmAgent into function parameters
31
+ 5. Handles topology updates when tools join/leave
32
+ """
33
+
34
+ def __init__(self):
35
+ """Initialize the LLM agent injector."""
36
+ super().__init__()
37
+ self._llm_agents: dict[str, dict[str, Any]] = {}
38
+
39
+ def _build_function_name_to_id_mapping(self) -> dict[str, str]:
40
+ """
41
+ Build mapping from function_name to function_id.
42
+
43
+ Registry returns llm_tools keyed by function_name (e.g., "chat"),
44
+ but DecoratorRegistry stores LLM agents keyed by function_id (e.g., "chat_ac4ed56b").
45
+
46
+ Returns:
47
+ Dict mapping function_name -> function_id
48
+ """
49
+ llm_agents = DecoratorRegistry.get_mesh_llm_agents()
50
+ return {
51
+ metadata.function.__name__: function_id
52
+ for function_id, metadata in llm_agents.items()
53
+ }
54
+
55
+ def process_llm_tools(self, llm_tools: dict[str, list[dict[str, Any]]]) -> None:
56
+ """
57
+ Process llm_tools from registry response.
58
+
59
+ Creates UnifiedMCPProxy instances and MeshLlmAgent instances
60
+ for each function_id.
61
+
62
+ Args:
63
+ llm_tools: Dict mapping function_name -> list of tool metadata
64
+ Format: {"function_name": [{"function_name": "...", "endpoint": {...}, ...}]}
65
+ Note: Registry uses function_name as key, but we need to map to function_id
66
+ """
67
+ logger.info(f"🔧 Processing llm_tools for {len(llm_tools)} functions")
68
+
69
+ # Build mapping from function_name to function_id
70
+ function_name_to_id = self._build_function_name_to_id_mapping()
71
+
72
+ for function_name, tools in llm_tools.items():
73
+ try:
74
+ # Map function_name to function_id
75
+ if function_name not in function_name_to_id:
76
+ logger.warning(
77
+ f"⚠️ Function name '{function_name}' not found in DecoratorRegistry, skipping"
78
+ )
79
+ continue
80
+
81
+ function_id = function_name_to_id[function_name]
82
+ self._process_function_tools(function_id, tools)
83
+ except Exception as e:
84
+ logger.error(
85
+ f"❌ Error processing llm_tools for {function_name}: {e}",
86
+ exc_info=True,
87
+ )
88
+
89
+ def _process_function_tools(
90
+ self, function_id: str, tools: list[dict[str, Any]]
91
+ ) -> None:
92
+ """
93
+ Process tools for a single function.
94
+
95
+ Args:
96
+ function_id: Unique function ID from @mesh.llm decorator
97
+ tools: List of tool metadata from registry
98
+ """
99
+ # Get LLM agent metadata from DecoratorRegistry
100
+ llm_agents = DecoratorRegistry.get_mesh_llm_agents()
101
+
102
+ if function_id not in llm_agents:
103
+ logger.warning(
104
+ f"⚠️ Function '{function_id}' not found in DecoratorRegistry, skipping"
105
+ )
106
+ return
107
+
108
+ llm_metadata = llm_agents[function_id]
109
+
110
+ # Create UnifiedMCPProxy instances for each tool and build proxy map
111
+ tool_proxies_map = {} # Map function_name -> proxy
112
+ for tool in tools:
113
+ try:
114
+ proxy = self._create_tool_proxy(tool)
115
+ # OpenAPI spec uses "name" (camelCase) - enforce strict contract
116
+ function_name = tool.get("name")
117
+ if function_name:
118
+ tool_proxies_map[function_name] = proxy
119
+ else:
120
+ logger.error(
121
+ f"❌ Tool missing 'name' field (OpenAPI contract): {tool}"
122
+ )
123
+ except Exception as e:
124
+ # Get tool name for error message
125
+ tool_name = tool.get("name", "unknown")
126
+ logger.error(f"❌ Error creating proxy for tool {tool_name}: {e}")
127
+ # Continue processing other tools
128
+
129
+ # Store LLM agent data with both metadata and proxies
130
+ # Keep original tool metadata for schema building
131
+ self._llm_agents[function_id] = {
132
+ "config": llm_metadata.config,
133
+ "output_type": llm_metadata.output_type,
134
+ "param_name": llm_metadata.param_name,
135
+ "tools_metadata": tools, # Original metadata for schema building
136
+ "tools_proxies": tool_proxies_map, # Proxies for execution
137
+ "function": llm_metadata.function,
138
+ }
139
+
140
+ logger.info(
141
+ f"✅ Processed {len(tool_proxies_map)} tools for LLM function '{function_id}'"
142
+ )
143
+
144
+ # Update wrapper with MeshLlmAgent (two-phase pattern - Phase 2)
145
+ # Option A: Decorator stores function in DecoratorRegistry (not _function_registry)
146
+ # Get the function from DecoratorRegistry by matching function_id
147
+ llm_agents = DecoratorRegistry.get_mesh_llm_agents()
148
+ wrapper = None
149
+ for agent_func_id, metadata in llm_agents.items():
150
+ if metadata.function_id == function_id:
151
+ wrapper = metadata.function
152
+ break
153
+
154
+ if wrapper and hasattr(wrapper, "_mesh_update_llm_agent"):
155
+ llm_agent = self._create_llm_agent(function_id)
156
+ wrapper._mesh_update_llm_agent(llm_agent)
157
+ logger.info(f"🔄 Updated wrapper with MeshLlmAgent for '{function_id}'")
158
+ elif wrapper:
159
+ logger.warning(
160
+ f"⚠️ Wrapper for '{function_id}' found but has no _mesh_update_llm_agent method"
161
+ )
162
+ else:
163
+ logger.warning(
164
+ f"⚠️ No wrapper found for '{function_id}' - MeshLlmAgent not injected (decorator should have created it)"
165
+ )
166
+
167
+ def _create_tool_proxy(self, tool: dict[str, Any]) -> UnifiedMCPProxy:
168
+ """
169
+ Create UnifiedMCPProxy for a tool.
170
+
171
+ Args:
172
+ tool: Tool metadata from registry (must match OpenAPI spec field names)
173
+
174
+ Returns:
175
+ UnifiedMCPProxy instance
176
+
177
+ Raises:
178
+ ValueError: If endpoint is missing or invalid
179
+ """
180
+ # OpenAPI spec uses "name" (camelCase) - enforce strict contract
181
+ function_name = tool.get("name")
182
+ if not function_name:
183
+ raise ValueError(
184
+ f"Tool missing required 'name' field (OpenAPI contract): {tool}"
185
+ )
186
+
187
+ endpoint = tool.get("endpoint")
188
+ if not endpoint:
189
+ raise ValueError(f"Tool {function_name} missing endpoint")
190
+
191
+ # Registry returns endpoint as a string URL (e.g., "http://localhost:9091")
192
+ # Use it directly instead of parsing host/port
193
+ if not isinstance(endpoint, str):
194
+ raise ValueError(
195
+ f"Tool {function_name} has invalid endpoint (expected string): {endpoint}"
196
+ )
197
+
198
+ # Create proxy with endpoint URL
199
+ proxy = UnifiedMCPProxy(
200
+ endpoint=endpoint,
201
+ function_name=function_name,
202
+ kwargs_config={
203
+ "capability": tool.get("capability"),
204
+ },
205
+ )
206
+
207
+ logger.debug(f"🔧 Created proxy for {function_name} at {endpoint}")
208
+
209
+ return proxy
210
+
211
+ def create_injection_wrapper(self, func: Callable, function_id: str) -> Callable:
212
+ """
213
+ Create wrapper that injects MeshLlmAgent into function parameters.
214
+
215
+ Like McpMeshAgent injection, this creates a wrapper at decorator time with llm_agent=None,
216
+ which gets updated during heartbeat when tools arrive from registry.
217
+
218
+ Args:
219
+ func: Original function to wrap
220
+ function_id: Unique function ID
221
+
222
+ Returns:
223
+ Wrapped function with MeshLlmAgent injection
224
+ """
225
+ # Get LLM metadata from DecoratorRegistry (registered at decorator time)
226
+ llm_agents = DecoratorRegistry.get_mesh_llm_agents()
227
+
228
+ if function_id not in llm_agents:
229
+ logger.warning(
230
+ f"⚠️ Function '{function_id}' not found in DecoratorRegistry, creating wrapper anyway"
231
+ )
232
+ # Get param_name from stored data if available, otherwise raise
233
+ if function_id in self._llm_agents:
234
+ param_name = self._llm_agents[function_id]["param_name"]
235
+ else:
236
+ raise ValueError(
237
+ f"Function '{function_id}' not found in LLM agent registry"
238
+ )
239
+ else:
240
+ llm_metadata = llm_agents[function_id]
241
+ param_name = llm_metadata.param_name
242
+
243
+ # Validate parameter exists
244
+ sig = inspect.signature(func)
245
+ if param_name not in sig.parameters:
246
+ raise ValueError(
247
+ f"Function '{func.__name__}' missing MeshLlmAgent parameter '{param_name}'"
248
+ )
249
+
250
+ # Initialize with None (will be updated during heartbeat)
251
+ llm_agent = None
252
+
253
+ # Create injection logic closure
254
+ def inject_llm_agent(func: Callable, args: tuple, kwargs: dict) -> tuple:
255
+ """Inject LLM agent into kwargs if not provided."""
256
+ if param_name not in kwargs or kwargs.get(param_name) is None:
257
+ # Phase 4: Check if templates are enabled
258
+ if function_id in self._llm_agents:
259
+ agent_data = self._llm_agents[function_id]
260
+ config_dict = agent_data["config"]
261
+ is_template = config_dict.get("is_template", False)
262
+
263
+ if is_template:
264
+ # Templates enabled - create per-call agent with context
265
+ # Import signature analyzer for context detection
266
+ from .signature_analyzer import get_context_parameter_name
267
+
268
+ # Detect context parameter
269
+ context_param_name = config_dict.get("context_param")
270
+ context_info = get_context_parameter_name(
271
+ func, explicit_name=context_param_name
272
+ )
273
+
274
+ # Extract context value from call
275
+ context_value = None
276
+ if context_info is not None:
277
+ ctx_name, ctx_index = context_info
278
+
279
+ # Try kwargs first
280
+ if ctx_name in kwargs:
281
+ context_value = kwargs[ctx_name]
282
+ # Then try positional args
283
+ elif ctx_index < len(args):
284
+ context_value = args[ctx_index]
285
+
286
+ # Create agent with context for this call
287
+ current_agent = self._create_llm_agent(
288
+ function_id, context_value=context_value
289
+ )
290
+ logger.debug(
291
+ f"🤖 Created MeshLlmAgent with context for {func.__name__}.{param_name}"
292
+ )
293
+ else:
294
+ # No template - use cached agent (existing behavior)
295
+ current_agent = wrapper._mesh_llm_agent
296
+ if current_agent is not None:
297
+ logger.debug(
298
+ f"🤖 Injected MeshLlmAgent into {func.__name__}.{param_name}"
299
+ )
300
+ else:
301
+ logger.warning(
302
+ f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (tools not yet received from registry)"
303
+ )
304
+ else:
305
+ # No agent data - use cached (backward compatibility)
306
+ current_agent = wrapper._mesh_llm_agent
307
+ if current_agent is None:
308
+ logger.warning(
309
+ f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (tools not yet received from registry)"
310
+ )
311
+
312
+ kwargs[param_name] = current_agent
313
+ return args, kwargs
314
+
315
+ # Create update method closure
316
+ def update_llm_agent(agent: MeshLlmAgent | None) -> None:
317
+ wrapper._mesh_llm_agent = agent
318
+ logger.info(
319
+ f"🔄 Updated MeshLlmAgent for {func.__name__} (function_id={function_id})"
320
+ )
321
+
322
+ # Prepare metadata
323
+ metadata = {
324
+ "_mesh_llm_agent": llm_agent,
325
+ "_mesh_param_name": param_name,
326
+ "_mesh_update_llm_agent": update_llm_agent,
327
+ }
328
+
329
+ # Use base class to create wrapper (handles async/sync automatically)
330
+ wrapper = self.create_wrapper_with_injection(
331
+ func, function_id, inject_llm_agent, metadata, register=True
332
+ )
333
+
334
+ logger.debug(
335
+ f"✅ Created LLM injection wrapper for {func.__name__} (agent=None, will be updated during heartbeat)"
336
+ )
337
+
338
+ return wrapper
339
+
340
+ def _create_llm_agent(
341
+ self, function_id: str, context_value: Any = None
342
+ ) -> MeshLlmAgent:
343
+ """
344
+ Create MeshLlmAgent instance for a function.
345
+
346
+ Args:
347
+ function_id: Unique function ID
348
+ context_value: Optional context for template rendering (Phase 4)
349
+
350
+ Returns:
351
+ MeshLlmAgent instance configured with tools and config
352
+ """
353
+ llm_agent_data = self._llm_agents[function_id]
354
+ config_dict = llm_agent_data["config"]
355
+
356
+ # Create LLMConfig from dict
357
+ llm_config = LLMConfig(
358
+ provider=config_dict.get("provider", "claude"),
359
+ model=config_dict.get("model", "claude-3-5-sonnet-20241022"),
360
+ api_key=config_dict.get("api_key", ""), # Will use ENV if empty
361
+ max_iterations=config_dict.get("max_iterations", 10),
362
+ system_prompt=config_dict.get("system_prompt"),
363
+ )
364
+
365
+ # Phase 4: Template support - extract template metadata
366
+ is_template = config_dict.get("is_template", False)
367
+ template_path = config_dict.get("template_path")
368
+
369
+ # Create MeshLlmAgent with both metadata and proxies
370
+ llm_agent = MeshLlmAgent(
371
+ config=llm_config,
372
+ filtered_tools=llm_agent_data[
373
+ "tools_metadata"
374
+ ], # Metadata for schema building
375
+ output_type=llm_agent_data["output_type"],
376
+ tool_proxies=llm_agent_data["tools_proxies"], # Proxies for execution
377
+ template_path=template_path if is_template else None,
378
+ context_value=context_value if is_template else None,
379
+ )
380
+
381
+ logger.debug(
382
+ f"🤖 Created MeshLlmAgent for {function_id} with {len(llm_agent_data['tools_metadata'])} tools"
383
+ + (f", template={template_path}" if is_template else "")
384
+ )
385
+
386
+ return llm_agent
387
+
388
+ def update_llm_tools(self, llm_tools: dict[str, list[dict[str, Any]]]) -> None:
389
+ """
390
+ Update LLM tools when topology changes.
391
+
392
+ Handles:
393
+ - New tools being added
394
+ - Existing tools being removed
395
+ - Entire functions being removed
396
+
397
+ Args:
398
+ llm_tools: Updated llm_tools dict from registry (keyed by function_name)
399
+ """
400
+ logger.info(f"🔄 Updating llm_tools for {len(llm_tools)} functions")
401
+
402
+ # Build mapping from function_name to function_id
403
+ function_name_to_id = self._build_function_name_to_id_mapping()
404
+
405
+ # Map function_names from registry to function_ids
406
+ current_function_ids = set()
407
+ for function_name in llm_tools.keys():
408
+ if function_name in function_name_to_id:
409
+ current_function_ids.add(function_name_to_id[function_name])
410
+
411
+ # Track which functions are still active
412
+ previous_functions = set(self._llm_agents.keys())
413
+
414
+ # Remove functions that are no longer in the topology
415
+ removed_functions = previous_functions - current_function_ids
416
+ for function_id in removed_functions:
417
+ logger.info(
418
+ f"🗑️ Removing LLM function {function_id} (no longer in topology)"
419
+ )
420
+ del self._llm_agents[function_id]
421
+ # Also remove from function registry if present
422
+ if function_id in self._function_registry:
423
+ del self._function_registry[function_id]
424
+
425
+ # Update or add functions
426
+ for function_name, tools in llm_tools.items():
427
+ try:
428
+ # Map function_name to function_id
429
+ if function_name not in function_name_to_id:
430
+ logger.warning(
431
+ f"⚠️ Function name '{function_name}' not found in DecoratorRegistry during update, skipping"
432
+ )
433
+ continue
434
+
435
+ function_id = function_name_to_id[function_name]
436
+
437
+ # Reprocess tools (will update existing or create new)
438
+ self._process_function_tools(function_id, tools)
439
+
440
+ # Update existing wrappers if they exist
441
+ if function_id in self._function_registry:
442
+ wrapper = self._function_registry[function_id]
443
+ # Recreate LLM agent with updated tools
444
+ new_llm_agent = self._create_llm_agent(function_id)
445
+ wrapper._mesh_llm_agent = new_llm_agent
446
+ logger.debug(
447
+ f"🔄 Updated MeshLlmAgent for existing wrapper: {function_id}"
448
+ )
449
+
450
+ except Exception as e:
451
+ logger.error(
452
+ f"❌ Error updating llm_tools for {function_name}: {e}",
453
+ exc_info=True,
454
+ )
455
+
456
+ logger.info(
457
+ f"✅ LLM tools update complete: {len(self._llm_agents)} functions active"
458
+ )
459
+
460
+ def get_llm_agent_data(self, function_id: str) -> dict[str, Any] | None:
461
+ """
462
+ Get LLM agent data for a function.
463
+
464
+ Args:
465
+ function_id: Unique function ID
466
+
467
+ Returns:
468
+ LLM agent data dict or None if not found
469
+ """
470
+ return self._llm_agents.get(function_id)
471
+
472
+
473
+ # Global injector instance
474
+ _global_llm_injector: MeshLlmAgentInjector | None = None
475
+
476
+
477
+ def get_global_llm_injector() -> MeshLlmAgentInjector:
478
+ """
479
+ Get or create the global MeshLlmAgentInjector instance.
480
+
481
+ Returns:
482
+ Global MeshLlmAgentInjector instance
483
+ """
484
+ global _global_llm_injector
485
+ if _global_llm_injector is None:
486
+ _global_llm_injector = MeshLlmAgentInjector()
487
+ return _global_llm_injector