Spaces:
Running
Running
| """Constructs prompts for servers and tools, formats tool information for OpenAI API.""" | |
| from typing import Dict, Optional, List, Tuple, Any | |
| from loguru import logger | |
| from .types import FormattedTool | |
| from .mcp_client import MCPClient | |
| from .server_registry import ServerRegistry | |
| class ToolAdapter: | |
| """Dynamically fetches tool information from enabled MCP servers and formats it.""" | |
| def __init__(self, server_registery: Optional[ServerRegistry] = None) -> None: | |
| """Initialize with an ServerRegistry.""" | |
| self.server_registery = server_registery or ServerRegistry() | |
| async def get_server_and_tool_info( | |
| self, enabled_servers: List[str] | |
| ) -> Tuple[Dict[str, Dict[str, str]], Dict[str, FormattedTool]]: | |
| """Fetch tool information from specified enabled MCP servers.""" | |
| servers_info: Dict[str, Dict[str, str]] = {} | |
| formatted_tools: Dict[str, FormattedTool] = {} | |
| if not enabled_servers: | |
| logger.warning( | |
| "MC: No enabled MCP servers specified. Cannot fetch tool info." | |
| ) | |
| return servers_info, formatted_tools | |
| logger.debug(f"MC: Fetching tool info for enabled servers: {enabled_servers}") | |
| # Use a single client instance for efficiency | |
| async with MCPClient(self.server_registery) as client: | |
| for server_name in enabled_servers: | |
| if server_name not in self.server_registery.servers: | |
| logger.warning( | |
| f"MC: Enabled server '{server_name}' not found in Server Manager. Skipping." | |
| ) | |
| continue | |
| try: | |
| servers_info[server_name] = {} | |
| tools = await client.list_tools(server_name) | |
| logger.debug( | |
| f"MC: Found {len(tools)} tools on server '{server_name}'" | |
| ) | |
| for tool in tools: | |
| servers_info[server_name][tool.name] = {} | |
| tool_info = servers_info[server_name][tool.name] | |
| tool_info["description"] = tool.description | |
| tool_info["parameters"] = tool.inputSchema.get("properties", {}) | |
| tool_info["required"] = tool.inputSchema.get("required", []) | |
| # Store the tool info in FormattedTool format | |
| formatted_tools[tool.name] = FormattedTool( | |
| input_schema=tool.inputSchema, | |
| related_server=server_name, | |
| description=tool.description, | |
| # Generic schema will be generated later if needed | |
| generic_schema=None, | |
| ) | |
| except (ValueError, RuntimeError, ConnectionError) as e: | |
| logger.error( | |
| f"MC: Failed to get info for server '{server_name}': {e}" | |
| ) | |
| if ( | |
| server_name not in servers_info | |
| ): # Ensure entry exists even on error | |
| servers_info[server_name] = {} | |
| continue # Continue to next server | |
| except Exception as e: | |
| logger.error( | |
| f"MC: Unexpected error for server '{server_name}': {e}" | |
| ) | |
| if server_name not in servers_info: | |
| servers_info[server_name] = {} | |
| continue # Continue to next server | |
| logger.debug( | |
| f"MC: Finished fetching tool info. Found {len(formatted_tools)} tools across enabled servers." | |
| ) | |
| return servers_info, formatted_tools | |
| def construct_mcp_prompt_string( | |
| self, servers_info: Dict[str, Dict[str, str]] | |
| ) -> str: | |
| """Build a single prompt string describing enabled servers and their tools.""" | |
| full_prompt_content = "" | |
| if not servers_info: | |
| logger.warning( | |
| "MC: Cannot construct MCP prompt string, servers_info is empty." | |
| ) | |
| return full_prompt_content | |
| logger.debug( | |
| f"MC: Constructing MCP prompt string for {len(servers_info)} server(s)." | |
| ) | |
| for server_name, tools in servers_info.items(): | |
| if not tools: # Skip servers where info couldn't be fetched | |
| logger.warning( | |
| f"MC: No tool info available for server '{server_name}', skipping in prompt." | |
| ) | |
| continue | |
| prompt_content = f"Server: {server_name}\n" | |
| prompt_content += " Tools:\n" | |
| for tool_name, tool_info in tools.items(): | |
| prompt_content += f" {tool_name}:\n" | |
| # Ensure description is handled correctly (might be None) | |
| description = tool_info.get("description", "No description available.") | |
| prompt_content += f" Description: {description}\n" | |
| parameters = tool_info.get("parameters", {}) | |
| if parameters: | |
| prompt_content += " Parameters:\n" | |
| for param_name, param_info in parameters.items(): | |
| param_desc = param_info.get("description") or param_info.get( | |
| "title", "No description provided." | |
| ) | |
| param_type = param_info.get( | |
| "type", "string" | |
| ) # Default to string if type missing | |
| prompt_content += f" {param_name}:\n" | |
| prompt_content += f" Type: {param_type}\n" | |
| prompt_content += ( | |
| f" Description: {param_desc}\n" | |
| ) | |
| required = tool_info.get("required", []) | |
| if required: | |
| prompt_content += f" Required: {', '.join(required)}\n" | |
| full_prompt_content += prompt_content + "\n" # Add newline between servers | |
| logger.debug("MC: Finished constructing MCP prompt string.") | |
| return full_prompt_content.strip() # Remove trailing newline | |
| def format_tools_for_api( | |
| self, formatted_tools_dict: Dict[str, FormattedTool] | |
| ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: | |
| """Format tools to OpenAI and Claude function-calling compatible schemas.""" | |
| openai_tools = [] | |
| claude_tools = [] | |
| if not formatted_tools_dict: | |
| logger.warning( | |
| "MC: Cannot format tools for API, input dictionary is empty." | |
| ) | |
| return openai_tools, claude_tools | |
| logger.debug(f"MC: Formatting {len(formatted_tools_dict)} tools for API usage.") | |
| for tool_name, data_object in formatted_tools_dict.items(): | |
| if not isinstance(data_object, FormattedTool): | |
| logger.warning(f"MC: Skipping invalid tool format for '{tool_name}'") | |
| continue | |
| input_schema = data_object.input_schema | |
| properties: Dict[str, Dict[str, str]] = input_schema.get("properties", {}) | |
| tool_description = data_object.description or "No description provided." | |
| required_params = input_schema.get("required", []) | |
| # Format for OpenAI | |
| openai_function_params = { | |
| "type": "object", | |
| "properties": {}, | |
| "required": required_params, | |
| "additionalProperties": False, # Disallow extra properties | |
| } | |
| for param_name, param_info in properties.items(): | |
| param_schema = { | |
| "type": param_info.get("type", "string"), | |
| "description": param_info.get("description") | |
| or param_info.get("title", "No description provided."), | |
| } | |
| # Add enum if present | |
| if "enum" in param_info: | |
| param_schema["enum"] = param_info["enum"] | |
| # Handle array type correctly | |
| if param_schema["type"] == "array" and "items" in param_info: | |
| param_schema["items"] = param_info["items"] | |
| elif param_schema["type"] == "array" and "items" not in param_info: | |
| logger.warning( | |
| f"MC: Array parameter '{param_name}' in tool '{tool_name}' is missing 'items' definition. Assuming items are strings." | |
| ) | |
| param_schema["items"] = {"type": "string"} # Default or log warning | |
| openai_function_params["properties"][param_name] = param_schema | |
| openai_tools.append( | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": tool_name, | |
| "description": tool_description, | |
| "parameters": openai_function_params, | |
| }, | |
| } | |
| ) | |
| # Format for Claude | |
| claude_input_schema = { | |
| "type": "object", | |
| "properties": properties, | |
| "required": required_params, | |
| } | |
| claude_tools.append( | |
| { | |
| "name": tool_name, | |
| "description": tool_description, | |
| "input_schema": claude_input_schema, | |
| } | |
| ) | |
| logger.debug( | |
| f"MC: Finished formatting tools. OpenAI: {len(openai_tools)}, Claude: {len(claude_tools)}." | |
| ) | |
| return openai_tools, claude_tools | |
| async def get_tools( | |
| self, enabled_servers: List[str] | |
| ) -> Tuple[str, List[Dict[str, Any]], List[Dict[str, Any]]]: | |
| """Run the dynamic fetching and formatting process.""" | |
| logger.info( | |
| f"MC: Running dynamic tool construction for servers: {enabled_servers}" | |
| ) | |
| servers_info, formatted_tools_dict = await self.get_server_and_tool_info( | |
| enabled_servers | |
| ) | |
| mcp_prompt_string = self.construct_mcp_prompt_string(servers_info) | |
| openai_tools, claude_tools = self.format_tools_for_api(formatted_tools_dict) | |
| logger.info("MC: Dynamic tool construction complete.") | |
| return mcp_prompt_string, openai_tools, claude_tools | |