import json import httpx from typing import Optional, Dict, Any, List from agentpress.tool import Tool, ToolResult, openapi_schema, xml_schema from agentpress.thread_manager import ThreadManager class UpdateAgentTool(Tool): """Tool for updating agent configuration. This tool is used by the agent builder to update agent properties based on user requirements. """ def __init__(self, thread_manager: ThreadManager, db_connection, agent_id: str): super().__init__() self.thread_manager = thread_manager self.db = db_connection self.agent_id = agent_id # Smithery API configuration self.smithery_api_base_url = "https://registry.smithery.ai" import os self.smithery_api_key = os.getenv("SMITHERY_API_KEY") @openapi_schema({ "type": "function", "function": { "name": "update_agent", "description": "Update the agent's configuration including name, description, system prompt, tools, and MCP servers. Call this whenever the user wants to modify any aspect of the agent.", "parameters": { "type": "object", "properties": { "name": { "type": "string", "description": "The name of the agent. Should be descriptive and indicate the agent's purpose." }, "description": { "type": "string", "description": "A brief description of what the agent does and its capabilities." }, "system_prompt": { "type": "string", "description": "The system instructions that define the agent's behavior, expertise, and approach. This should be comprehensive and well-structured." }, "agentpress_tools": { "type": "object", "description": "Configuration for AgentPress tools. Each key is a tool name, and the value is an object with 'enabled' (boolean) and 'description' (string) properties.", "additionalProperties": { "type": "object", "properties": { "enabled": {"type": "boolean"}, "description": {"type": "string"} } } }, "configured_mcps": { "type": "array", "description": "List of configured MCP servers for external integrations.", "items": { "type": "object", "properties": { "name": {"type": "string"}, "qualifiedName": {"type": "string"}, "config": {"type": "object"}, "enabledTools": { "type": "array", "items": {"type": "string"} } } } }, "avatar": { "type": "string", "description": "Emoji to use as the agent's avatar." }, "avatar_color": { "type": "string", "description": "Hex color code for the agent's avatar background." } }, "required": [] } } }) @xml_schema( tag_name="update-agent", mappings=[ {"param_name": "name", "node_type": "attribute", "path": ".", "required": False}, {"param_name": "description", "node_type": "element", "path": "description", "required": False}, {"param_name": "system_prompt", "node_type": "element", "path": "system_prompt", "required": False}, {"param_name": "agentpress_tools", "node_type": "element", "path": "agentpress_tools", "required": False}, {"param_name": "configured_mcps", "node_type": "element", "path": "configured_mcps", "required": False}, {"param_name": "avatar", "node_type": "attribute", "path": ".", "required": False}, {"param_name": "avatar_color", "node_type": "attribute", "path": ".", "required": False} ], example=''' Research Assistant An AI assistant specialized in conducting research and providing comprehensive analysis You are a research assistant with expertise in gathering, analyzing, and synthesizing information. Your approach is thorough and methodical... {"web_search": {"enabled": true, "description": "Search the web for information"}, "sb_files": {"enabled": true, "description": "Read and write files"}} 🔬 #4F46E5 ''' ) async def update_agent( self, name: Optional[str] = None, description: Optional[str] = None, system_prompt: Optional[str] = None, agentpress_tools: Optional[Dict[str, Dict[str, Any]]] = None, configured_mcps: Optional[list] = None, avatar: Optional[str] = None, avatar_color: Optional[str] = None ) -> ToolResult: """Update agent configuration with provided fields. Args: name: Agent name description: Agent description system_prompt: System instructions for the agent agentpress_tools: AgentPress tools configuration configured_mcps: MCP servers configuration avatar: Emoji avatar avatar_color: Avatar background color Returns: ToolResult with updated agent data or error """ try: client = await self.db.client update_data = {} if name is not None: update_data["name"] = name if description is not None: update_data["description"] = description if system_prompt is not None: update_data["system_prompt"] = system_prompt if agentpress_tools is not None: formatted_tools = {} for tool_name, tool_config in agentpress_tools.items(): if isinstance(tool_config, dict): formatted_tools[tool_name] = { "enabled": tool_config.get("enabled", False), "description": tool_config.get("description", "") } update_data["agentpress_tools"] = formatted_tools if configured_mcps is not None: if isinstance(configured_mcps, str): configured_mcps = json.loads(configured_mcps) update_data["configured_mcps"] = configured_mcps if avatar is not None: update_data["avatar"] = avatar if avatar_color is not None: update_data["avatar_color"] = avatar_color if not update_data: return self.fail_response("No fields provided to update") result = await client.table('agents').update(update_data).eq('agent_id', self.agent_id).execute() if not result.data: return self.fail_response("Failed to update agent") return self.success_response({ "message": "Agent updated successfully", "updated_fields": list(update_data.keys()), "agent": result.data[0] }) except Exception as e: return self.fail_response(f"Error updating agent: {str(e)}") @openapi_schema({ "type": "function", "function": { "name": "get_current_agent_config", "description": "Get the current configuration of the agent being edited. Use this to check what's already configured before making updates.", "parameters": { "type": "object", "properties": {}, "required": [] } } }) @xml_schema( tag_name="get-current-agent-config", mappings=[], example=''' ''' ) async def get_current_agent_config(self) -> ToolResult: """Get the current agent configuration. Returns: ToolResult with current agent configuration """ try: client = await self.db.client result = await client.table('agents').select('*').eq('agent_id', self.agent_id).execute() if not result.data: return self.fail_response("Agent not found") agent = result.data[0] config_summary = { "agent_id": agent["agent_id"], "name": agent.get("name", "Untitled Agent"), "description": agent.get("description", "No description set"), "system_prompt": agent.get("system_prompt", "No system prompt set"), "avatar": agent.get("avatar", "🤖"), "avatar_color": agent.get("avatar_color", "#6B7280"), "agentpress_tools": agent.get("agentpress_tools", {}), "configured_mcps": agent.get("configured_mcps", []), "created_at": agent.get("created_at"), "updated_at": agent.get("updated_at") } tools_count = len([t for t, cfg in config_summary["agentpress_tools"].items() if cfg.get("enabled")]) mcps_count = len(config_summary["configured_mcps"]) return self.success_response({ "summary": f"Agent '{config_summary['name']}' has {tools_count} tools enabled and {mcps_count} MCP servers configured.", "configuration": config_summary }) except Exception as e: return self.fail_response(f"Error getting agent configuration: {str(e)}") @openapi_schema({ "type": "function", "function": { "name": "search_mcp_servers", "description": "Search for MCP servers from the Smithery registry based on user requirements. Use this when the user wants to add MCP tools to their agent.", "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Search query for finding relevant MCP servers (e.g., 'linear', 'github', 'database', 'search')" }, "category": { "type": "string", "description": "Optional category filter", "enum": ["AI & Search", "Development & Version Control", "Project Management", "Communication & Collaboration", "Data & Analytics", "Cloud & Infrastructure", "File Storage", "Marketing & Sales", "Customer Support", "Finance", "Automation & Productivity", "Utilities"] }, "limit": { "type": "integer", "description": "Maximum number of servers to return (default: 10)", "default": 10 } }, "required": ["query"] } } }) @xml_schema( tag_name="search-mcp-servers", mappings=[ {"param_name": "query", "node_type": "attribute", "path": "."}, {"param_name": "category", "node_type": "attribute", "path": "."}, {"param_name": "limit", "node_type": "attribute", "path": "."} ], example=''' linear 5 ''' ) async def search_mcp_servers( self, query: str, category: Optional[str] = None, limit: int = 10 ) -> ToolResult: """Search for MCP servers based on user requirements. Args: query: Search query for finding relevant MCP servers category: Optional category filter limit: Maximum number of servers to return Returns: ToolResult with matching MCP servers """ try: async with httpx.AsyncClient() as client: headers = { "Accept": "application/json", "User-Agent": "Suna-MCP-Integration/1.0" } if self.smithery_api_key: headers["Authorization"] = f"Bearer {self.smithery_api_key}" params = { "q": query, "page": 1, "pageSize": min(limit * 2, 50) # Get more results to filter } response = await client.get( f"{self.smithery_api_base_url}/servers", headers=headers, params=params, timeout=30.0 ) response.raise_for_status() data = response.json() servers = data.get("servers", []) # Filter by category if specified if category: filtered_servers = [] for server in servers: server_category = self._categorize_server(server) if server_category == category: filtered_servers.append(server) servers = filtered_servers # Sort by useCount and limit results servers = sorted(servers, key=lambda x: x.get("useCount", 0), reverse=True)[:limit] # Format results for user-friendly display formatted_servers = [] for server in servers: formatted_servers.append({ "name": server.get("displayName", server.get("qualifiedName", "Unknown")), "qualifiedName": server.get("qualifiedName"), "description": server.get("description", "No description available"), "useCount": server.get("useCount", 0), "category": self._categorize_server(server), "homepage": server.get("homepage", ""), "isDeployed": server.get("isDeployed", False) }) if not formatted_servers: return ToolResult( success=False, output=json.dumps([], ensure_ascii=False) ) return ToolResult( success=True, output=json.dumps(formatted_servers, ensure_ascii=False) ) except Exception as e: return self.fail_response(f"Error searching MCP servers: {str(e)}") @openapi_schema({ "type": "function", "function": { "name": "get_mcp_server_tools", "description": "Get detailed information about a specific MCP server including its available tools. Use this after the user selects a server they want to connect to.", "parameters": { "type": "object", "properties": { "qualified_name": { "type": "string", "description": "The qualified name of the MCP server (e.g., 'exa', '@smithery-ai/github')" } }, "required": ["qualified_name"] } } }) @xml_schema( tag_name="get-mcp-server-tools", mappings=[ {"param_name": "qualified_name", "node_type": "attribute", "path": ".", "required": True} ], example=''' exa ''' ) async def get_mcp_server_tools(self, qualified_name: str) -> ToolResult: """Get detailed information about a specific MCP server and its tools. Args: qualified_name: The qualified name of the MCP server Returns: ToolResult with server details and available tools """ try: # First get server metadata from registry async with httpx.AsyncClient() as client: headers = { "Accept": "application/json", "User-Agent": "Suna-MCP-Integration/1.0" } if self.smithery_api_key: headers["Authorization"] = f"Bearer {self.smithery_api_key}" # URL encode the qualified name if it contains special characters from urllib.parse import quote if '@' in qualified_name or '/' in qualified_name: encoded_name = quote(qualified_name, safe='') else: encoded_name = qualified_name url = f"{self.smithery_api_base_url}/servers/{encoded_name}" response = await client.get( url, headers=headers, timeout=30.0 ) response.raise_for_status() server_data = response.json() # Now connect to the MCP server to get actual tools using ClientSession try: # Import MCP components from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client import base64 import os # Check if Smithery API key is available smithery_api_key = os.getenv("SMITHERY_API_KEY") if not smithery_api_key: raise ValueError("SMITHERY_API_KEY environment variable is not set") # Create server URL with empty config for testing config_json = json.dumps({}) config_b64 = base64.b64encode(config_json.encode()).decode() server_url = f"https://server.smithery.ai/{qualified_name}/mcp?config={config_b64}&api_key={smithery_api_key}" # Connect and get tools async with streamablehttp_client(server_url) as (read_stream, write_stream, _): async with ClientSession(read_stream, write_stream) as session: # Initialize the connection await session.initialize() # List available tools tools_result = await session.list_tools() tools = tools_result.tools if hasattr(tools_result, 'tools') else tools_result # Format tools for user-friendly display formatted_tools = [] for tool in tools: tool_info = { "name": tool.name, "description": getattr(tool, 'description', 'No description available'), } # Extract parameters from inputSchema if available if hasattr(tool, 'inputSchema') and tool.inputSchema: schema = tool.inputSchema if isinstance(schema, dict): tool_info["parameters"] = schema.get("properties", {}) tool_info["required_params"] = schema.get("required", []) else: tool_info["parameters"] = {} tool_info["required_params"] = [] else: tool_info["parameters"] = {} tool_info["required_params"] = [] formatted_tools.append(tool_info) # Extract configuration requirements from server metadata config_requirements = [] security = server_data.get("security", {}) if security: for key, value in security.items(): if isinstance(value, dict): config_requirements.append({ "name": key, "description": value.get("description", f"Configuration for {key}"), "required": value.get("required", False), "type": value.get("type", "string") }) server_info = { "name": server_data.get("displayName", qualified_name), "qualifiedName": qualified_name, "description": server_data.get("description", "No description available"), "homepage": server_data.get("homepage", ""), "iconUrl": server_data.get("iconUrl", ""), "isDeployed": server_data.get("isDeployed", False), "tools": formatted_tools, "config_requirements": config_requirements, "total_tools": len(formatted_tools) } return self.success_response({ "message": f"Found {len(formatted_tools)} tools for {server_info['name']}", "server": server_info }) except Exception as mcp_error: # If MCP connection fails, fall back to registry data tools = server_data.get("tools", []) formatted_tools = [] for tool in tools: formatted_tools.append({ "name": tool.get("name", "Unknown"), "description": tool.get("description", "No description available"), "parameters": tool.get("inputSchema", {}).get("properties", {}), "required_params": tool.get("inputSchema", {}).get("required", []) }) config_requirements = [] security = server_data.get("security", {}) if security: for key, value in security.items(): if isinstance(value, dict): config_requirements.append({ "name": key, "description": value.get("description", f"Configuration for {key}"), "required": value.get("required", False), "type": value.get("type", "string") }) server_info = { "name": server_data.get("displayName", qualified_name), "qualifiedName": qualified_name, "description": server_data.get("description", "No description available"), "homepage": server_data.get("homepage", ""), "iconUrl": server_data.get("iconUrl", ""), "isDeployed": server_data.get("isDeployed", False), "tools": formatted_tools, "config_requirements": config_requirements, "total_tools": len(formatted_tools), "note": "Tools listed from registry metadata (MCP connection failed - may need configuration)" } return self.success_response({ "message": f"Found {len(formatted_tools)} tools for {server_info['name']} (from registry)", "server": server_info }) except Exception as e: return self.fail_response(f"Error getting MCP server tools: {str(e)}") @openapi_schema({ "type": "function", "function": { "name": "configure_mcp_server", "description": "Configure and add an MCP server to the agent with selected tools. Use this after the user has chosen which tools they want from a server.", "parameters": { "type": "object", "properties": { "qualified_name": { "type": "string", "description": "The qualified name of the MCP server" }, "display_name": { "type": "string", "description": "Display name for the server" }, "enabled_tools": { "type": "array", "description": "List of tool names to enable for this server", "items": {"type": "string"} }, "config": { "type": "object", "description": "Configuration object with API keys and other settings", "additionalProperties": True } }, "required": ["qualified_name", "display_name", "enabled_tools"] } } }) @xml_schema( tag_name="configure-mcp-server", mappings=[ {"param_name": "qualified_name", "node_type": "attribute", "path": ".", "required": True}, {"param_name": "display_name", "node_type": "attribute", "path": ".", "required": True}, {"param_name": "enabled_tools", "node_type": "element", "path": "enabled_tools", "required": True}, {"param_name": "config", "node_type": "element", "path": "config", "required": False} ], example=''' exa Exa Search ["search", "find_similar"] {"exaApiKey": "user-api-key"} ''' ) async def configure_mcp_server( self, qualified_name: str, display_name: str, enabled_tools: List[str], config: Optional[Dict[str, Any]] = None ) -> ToolResult: """Configure and add an MCP server to the agent. Args: qualified_name: The qualified name of the MCP server display_name: Display name for the server enabled_tools: List of tool names to enable config: Configuration object with API keys and settings Returns: ToolResult with configuration status """ try: client = await self.db.client # Get current agent configuration result = await client.table('agents').select('configured_mcps').eq('agent_id', self.agent_id).execute() if not result.data: return self.fail_response("Agent not found") current_mcps = result.data[0].get('configured_mcps', []) # Check if server is already configured existing_server_index = None for i, mcp in enumerate(current_mcps): if mcp.get('qualifiedName') == qualified_name: existing_server_index = i break # Create new MCP configuration new_mcp_config = { "name": display_name, "qualifiedName": qualified_name, "config": config or {}, "enabledTools": enabled_tools } # Update or add the configuration if existing_server_index is not None: current_mcps[existing_server_index] = new_mcp_config action = "updated" else: current_mcps.append(new_mcp_config) action = "added" # Save to database update_result = await client.table('agents').update({ 'configured_mcps': current_mcps }).eq('agent_id', self.agent_id).execute() if not update_result.data: return self.fail_response("Failed to save MCP configuration") return self.success_response({ "message": f"Successfully {action} MCP server '{display_name}' with {len(enabled_tools)} tools", "server": new_mcp_config, "total_mcp_servers": len(current_mcps), "action": action }) except Exception as e: return self.fail_response(f"Error configuring MCP server: {str(e)}") @openapi_schema({ "type": "function", "function": { "name": "get_popular_mcp_servers", "description": "Get a list of popular and recommended MCP servers organized by category. Use this to show users popular options when they want to add MCP tools.", "parameters": { "type": "object", "properties": { "category": { "type": "string", "description": "Optional category filter to show only servers from a specific category", "enum": ["AI & Search", "Development & Version Control", "Project Management", "Communication & Collaboration", "Data & Analytics", "Cloud & Infrastructure", "File Storage", "Marketing & Sales", "Customer Support", "Finance", "Automation & Productivity", "Utilities"] } }, "required": [] } } }) @xml_schema( tag_name="get-popular-mcp-servers", mappings=[ {"param_name": "category", "node_type": "attribute", "path": ".", "required": False} ], example=''' AI & Search ''' ) async def get_popular_mcp_servers(self, category: Optional[str] = None) -> ToolResult: """Get popular MCP servers organized by category. Args: category: Optional category filter Returns: ToolResult with popular MCP servers """ try: async with httpx.AsyncClient() as client: headers = { "Accept": "application/json", "User-Agent": "Suna-MCP-Integration/1.0" } if self.smithery_api_key: headers["Authorization"] = f"Bearer {self.smithery_api_key}" response = await client.get( f"{self.smithery_api_base_url}/servers", headers=headers, params={"page": 1, "pageSize": 50}, timeout=30.0 ) response.raise_for_status() data = response.json() servers = data.get("servers", []) # Categorize servers categorized = {} for server in servers: server_category = self._categorize_server(server) if category and server_category != category: continue if server_category not in categorized: categorized[server_category] = [] categorized[server_category].append({ "name": server.get("displayName", server.get("qualifiedName", "Unknown")), "qualifiedName": server.get("qualifiedName"), "description": server.get("description", "No description available"), "useCount": server.get("useCount", 0), "homepage": server.get("homepage", ""), "isDeployed": server.get("isDeployed", False) }) # Sort categories and servers within each category for cat in categorized: categorized[cat] = sorted(categorized[cat], key=lambda x: x["useCount"], reverse=True)[:5] return self.success_response({ "message": f"Found popular MCP servers" + (f" in category '{category}'" if category else ""), "categorized_servers": categorized, "total_categories": len(categorized) }) except Exception as e: return self.fail_response(f"Error getting popular MCP servers: {str(e)}") def _categorize_server(self, server: Dict[str, Any]) -> str: """Categorize a server based on its qualified name and description.""" qualified_name = server.get("qualifiedName", "").lower() description = server.get("description", "").lower() # Category mappings category_mappings = { "AI & Search": ["exa", "perplexity", "openai", "anthropic", "duckduckgo", "brave", "google", "search"], "Development & Version Control": ["github", "gitlab", "bitbucket", "git"], "Project Management": ["linear", "jira", "asana", "notion", "trello", "monday", "clickup"], "Communication & Collaboration": ["slack", "discord", "teams", "zoom", "telegram"], "Data & Analytics": ["postgres", "mysql", "mongodb", "bigquery", "snowflake", "sqlite", "redis", "database"], "Cloud & Infrastructure": ["aws", "gcp", "azure", "vercel", "netlify", "cloudflare", "docker"], "File Storage": ["gdrive", "google-drive", "dropbox", "box", "onedrive", "s3", "drive"], "Marketing & Sales": ["hubspot", "salesforce", "mailchimp", "sendgrid"], "Customer Support": ["zendesk", "intercom", "freshdesk", "helpscout"], "Finance": ["stripe", "quickbooks", "xero", "plaid"], "Automation & Productivity": ["playwright", "puppeteer", "selenium", "desktop-commander", "sequential-thinking", "automation"], "Utilities": ["filesystem", "memory", "fetch", "time", "weather", "currency", "file"] } # Check qualified name and description for category keywords for category, keywords in category_mappings.items(): for keyword in keywords: if keyword in qualified_name or keyword in description: return category return "Other" @openapi_schema({ "type": "function", "function": { "name": "test_mcp_server_connection", "description": "Test connectivity to an MCP server with provided configuration. Use this to validate that a server can be connected to before adding it to the agent.", "parameters": { "type": "object", "properties": { "qualified_name": { "type": "string", "description": "The qualified name of the MCP server" }, "config": { "type": "object", "description": "Configuration object with API keys and other settings", "additionalProperties": True } }, "required": ["qualified_name"] } } }) @xml_schema( tag_name="test-mcp-server-connection", mappings=[ {"param_name": "qualified_name", "node_type": "attribute", "path": ".", "required": True}, {"param_name": "config", "node_type": "element", "path": "config", "required": False} ], example=''' exa {"exaApiKey": "user-api-key"} ''' ) async def test_mcp_server_connection( self, qualified_name: str, config: Optional[Dict[str, Any]] = None ) -> ToolResult: """Test connectivity to an MCP server with provided configuration. Args: qualified_name: The qualified name of the MCP server config: Configuration object with API keys and settings Returns: ToolResult with connection test results """ try: # Import MCP components from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client import base64 import os # Check if Smithery API key is available smithery_api_key = os.getenv("SMITHERY_API_KEY") if not smithery_api_key: return self.fail_response("SMITHERY_API_KEY environment variable is not set") # Create server URL with provided config config_json = json.dumps(config or {}) config_b64 = base64.b64encode(config_json.encode()).decode() server_url = f"https://server.smithery.ai/{qualified_name}/mcp?config={config_b64}&api_key={smithery_api_key}" # Test connection async with streamablehttp_client(server_url) as (read_stream, write_stream, _): async with ClientSession(read_stream, write_stream) as session: # Initialize the connection await session.initialize() # List available tools to verify connection tools_result = await session.list_tools() tools = tools_result.tools if hasattr(tools_result, 'tools') else tools_result tool_names = [tool.name for tool in tools] return self.success_response({ "message": f"Successfully connected to {qualified_name}", "qualified_name": qualified_name, "connection_status": "success", "available_tools": tool_names, "total_tools": len(tool_names) }) except Exception as e: return self.fail_response(f"Failed to connect to {qualified_name}: {str(e)}")