diff --git a/backend/agent/run.py b/backend/agent/run.py index de619748..3c6afb87 100644 --- a/backend/agent/run.py +++ b/backend/agent/run.py @@ -158,13 +158,12 @@ async def run_agent( mcp_info += "You have access to external MCP (Model Context Protocol) server tools through the call_mcp_tool function.\n" mcp_info += "To use an MCP tool, call it using the standard function calling format:\n" mcp_info += '\n' - mcp_info += '\n' - mcp_info += 'mcp_{server}_{tool}\n' + mcp_info += '\n' + mcp_info += '{server}_{tool}\n' mcp_info += '{"argument1": "value1", "argument2": "value2"}\n' mcp_info += '\n' mcp_info += '\n' - - # List configured MCP servers + mcp_info += "\nConfigured MCP servers:\n" for mcp_config in agent_config['configured_mcps']: server_name = mcp_config.get('name', 'Unknown') diff --git a/backend/agent/tools/mcp_tool_wrapper.py b/backend/agent/tools/mcp_tool_wrapper.py index 5c053a85..b2c02e43 100644 --- a/backend/agent/tools/mcp_tool_wrapper.py +++ b/backend/agent/tools/mcp_tool_wrapper.py @@ -2,7 +2,7 @@ MCP Tool Wrapper for AgentPress This module provides a generic tool wrapper that handles all MCP (Model Context Protocol) -server tool calls through a single AgentPress tool interface. +server tool calls through dynamically generated individual function methods. """ import json @@ -14,10 +14,10 @@ from utils.logger import logger class MCPToolWrapper(Tool): """ - A generic tool wrapper that handles all MCP server tool calls. + A generic tool wrapper that dynamically creates individual methods for each MCP tool. - This tool acts as a proxy, forwarding tool calls to the appropriate MCP server - based on the tool name prefix. + This tool creates separate function calls for each MCP tool while routing them all + through the same underlying implementation. """ def __init__(self, mcp_configs: Optional[List[Dict[str, Any]]] = None): @@ -31,13 +31,15 @@ class MCPToolWrapper(Tool): self.mcp_manager = MCPManager() self.mcp_configs = mcp_configs or [] self._initialized = False + self._dynamic_tools = {} async def _ensure_initialized(self): - """Ensure MCP connections are initialized.""" + """Ensure MCP connections are initialized and dynamic tools are created.""" if not self._initialized and self.mcp_configs: logger.info(f"Initializing MCP connections for {len(self.mcp_configs)} servers") try: await self.mcp_manager.connect_all(self.mcp_configs) + await self._create_dynamic_tools() self._initialized = True except ValueError as e: if "SMITHERY_API_KEY" in str(e): @@ -47,55 +49,65 @@ class MCPToolWrapper(Tool): logger.error("2. Set it as an environment variable: export SMITHERY_API_KEY='your-key-here'") logger.error("3. Or add it to your .env file: SMITHERY_API_KEY=your-key-here") raise + + async def _create_dynamic_tools(self): + """Create dynamic tool methods for each available MCP tool.""" + try: + available_tools = self.mcp_manager.get_all_tools_openapi() + + for tool_info in available_tools: + tool_name = tool_info.get('function', {}).get('name', '') + if tool_name: + # Create a dynamic method for this tool + self._create_dynamic_method(tool_name, tool_info) + + logger.info(f"Created {len(self._dynamic_tools)} dynamic MCP tool methods") + + except Exception as e: + logger.error(f"Error creating dynamic MCP tools: {e}") + + def _create_dynamic_method(self, tool_name: str, tool_info: Dict[str, Any]): + """Create a dynamic method for a specific MCP tool.""" + + async def dynamic_tool_method(arguments: Dict[str, Any]) -> ToolResult: + """Dynamically created method for MCP tool.""" + return await self._execute_mcp_tool(tool_name, arguments) + + # Store the method and its info + self._dynamic_tools[tool_name] = { + 'method': dynamic_tool_method, + 'info': tool_info + } + + # Add the method to this instance + setattr(self, tool_name.replace('-', '_'), dynamic_tool_method) + + def __getattr__(self, name: str): + """Handle calls to dynamically created MCP tool methods.""" + # Convert method name back to tool name (handle underscore conversion) + tool_name = name.replace('_', '-') + + if tool_name in self._dynamic_tools: + return self._dynamic_tools[tool_name]['method'] + + # If it looks like an MCP tool name, try to find it + for existing_tool_name in self._dynamic_tools: + if existing_tool_name.replace('-', '_') == name: + return self._dynamic_tools[existing_tool_name]['method'] + + raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") async def get_available_tools(self) -> List[Dict[str, Any]]: """Get all available MCP tools in OpenAPI format.""" await self._ensure_initialized() return self.mcp_manager.get_all_tools_openapi() - @openapi_schema({ - "type": "function", - "function": { - "name": "call_mcp_tool", - "description": "Execute a tool from any connected MCP server. This is a generic wrapper that forwards calls to MCP tools. The tool_name should be in the format 'mcp_{server}_{tool}' where {server} is the MCP server's qualified name and {tool} is the specific tool name.", - "parameters": { - "type": "object", - "properties": { - "tool_name": { - "type": "string", - "description": "The full MCP tool name in format 'mcp_{server}_{tool}', e.g., 'mcp_exa_web_search_exa'" - }, - "arguments": { - "type": "object", - "description": "The arguments to pass to the MCP tool, as a JSON object. The required arguments depend on the specific tool being called.", - "additionalProperties": True - } - }, - "required": ["tool_name", "arguments"] - } - } - }) - @xml_schema( - tag_name="call-mcp-tool", - mappings=[ - {"param_name": "tool_name", "node_type": "attribute", "path": "."}, - {"param_name": "arguments", "node_type": "content", "path": "."} - ], - example=''' - - - mcp_exa_web_search_exa - {"query": "latest developments in AI", "num_results": 10} - - - ''' - ) - async def call_mcp_tool(self, tool_name: str, arguments: Dict[str, Any]) -> ToolResult: + async def _execute_mcp_tool(self, tool_name: str, arguments: Dict[str, Any]) -> ToolResult: """ - Execute an MCP tool call. + Execute an MCP tool call (internal implementation). Args: - tool_name: The full MCP tool name (e.g., "mcp_exa_web_search_exa") + tool_name: The MCP tool name (e.g., "mcp_exa_web_search_exa") arguments: The arguments to pass to the tool Returns: @@ -107,13 +119,6 @@ class MCPToolWrapper(Tool): logger.info(f"Executing MCP tool {tool_name} with args: {arguments}") - # Parse tool name to extract server and tool info - parts = tool_name.split("_", 2) - if len(parts) != 3 or parts[0] != "mcp": - return self.fail_response(f"Invalid MCP tool name format: {tool_name}. Expected format: mcp_{{server}}_{{tool}}") - - _, server_name, original_tool_name = parts - # Parse arguments if they're provided as a JSON string if isinstance(arguments, str): try: @@ -124,6 +129,11 @@ class MCPToolWrapper(Tool): # Execute the tool through MCP manager result = await self.mcp_manager.execute_tool(tool_name, arguments) + # Parse tool name to extract server and tool info for metadata + parts = tool_name.split("_", 2) + server_name = parts[1] if len(parts) > 1 else "unknown" + original_tool_name = parts[2] if len(parts) > 2 else tool_name + # Enhance the result with metadata for better frontend display enhanced_result = { "mcp_metadata": { @@ -193,6 +203,57 @@ class MCPToolWrapper(Tool): } return self.fail_response(json.dumps(error_result, indent=2)) + + # Keep the original call_mcp_tool method as a fallback + @openapi_schema({ + "type": "function", + "function": { + "name": "call_mcp_tool", + "description": "Execute a tool from any connected MCP server. This is a fallback wrapper that forwards calls to MCP tools. The tool_name should be in the format 'mcp_{server}_{tool}' where {server} is the MCP server's qualified name and {tool} is the specific tool name.", + "parameters": { + "type": "object", + "properties": { + "tool_name": { + "type": "string", + "description": "The full MCP tool name in format 'mcp_{server}_{tool}', e.g., 'mcp_exa_web_search_exa'" + }, + "arguments": { + "type": "object", + "description": "The arguments to pass to the MCP tool, as a JSON object. The required arguments depend on the specific tool being called.", + "additionalProperties": True + } + }, + "required": ["tool_name", "arguments"] + } + } + }) + @xml_schema( + tag_name="call-mcp-tool", + mappings=[ + {"param_name": "tool_name", "node_type": "attribute", "path": "."}, + {"param_name": "arguments", "node_type": "content", "path": "."} + ], + example=''' + + + mcp_exa_web_search_exa + {"query": "latest developments in AI", "num_results": 10} + + + ''' + ) + async def call_mcp_tool(self, tool_name: str, arguments: Dict[str, Any]) -> ToolResult: + """ + Execute an MCP tool call (fallback method). + + Args: + tool_name: The full MCP tool name (e.g., "mcp_exa_web_search_exa") + arguments: The arguments to pass to the tool + + Returns: + ToolResult with the tool execution result + """ + return await self._execute_mcp_tool(tool_name, arguments) async def cleanup(self): """Disconnect all MCP servers."""