diff --git a/apps/miroflow-agent/.env.example b/apps/miroflow-agent/.env.example index 7b46f867..8a8fa69b 100644 --- a/apps/miroflow-agent/.env.example +++ b/apps/miroflow-agent/.env.example @@ -32,6 +32,9 @@ REASONING_BASE_URL="https://your_reasoning_base_url/v1/chat/completions" ANTHROPIC_API_KEY=your_anthropic_key ANTHROPIC_BASE_URL=https://api.anthropic.com +# API for Tavily Search (optional) +TAVILY_API_KEY=your_tavily_key + # API for Sogou Search (optional) TENCENTCLOUD_SECRET_ID=your_tencent_cloud_secret_id TENCENTCLOUD_SECRET_KEY=your_tencent_cloud_secret_key diff --git a/apps/miroflow-agent/conf/agent/default.yaml b/apps/miroflow-agent/conf/agent/default.yaml index 6d9ecca0..c478f2f7 100644 --- a/apps/miroflow-agent/conf/agent/default.yaml +++ b/apps/miroflow-agent/conf/agent/default.yaml @@ -14,6 +14,7 @@ sub_agents: agent-browsing: tools: - tool-google-search + # - tool-tavily-search # Alternative to tool-google-search (requires TAVILY_API_KEY) - tool-vqa - tool-reader - tool-python diff --git a/apps/miroflow-agent/src/config/settings.py b/apps/miroflow-agent/src/config/settings.py index d5489ddd..fe37977a 100644 --- a/apps/miroflow-agent/src/config/settings.py +++ b/apps/miroflow-agent/src/config/settings.py @@ -55,6 +55,9 @@ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1") +# API for Tavily Search (optional) +TAVILY_API_KEY = os.environ.get("TAVILY_API_KEY") + # API for Sogou Search TENCENTCLOUD_SECRET_ID = os.environ.get("TENCENTCLOUD_SECRET_ID") TENCENTCLOUD_SECRET_KEY = os.environ.get("TENCENTCLOUD_SECRET_KEY") @@ -136,6 +139,31 @@ def create_mcp_server_parameters(cfg: DictConfig, agent_cfg: DictConfig): } ) + if ( + agent_cfg.get("tools", None) is not None + and "tool-tavily-search" in agent_cfg["tools"] + ): + if not TAVILY_API_KEY: + raise ValueError( + "TAVILY_API_KEY is required when tool-tavily-search is configured but is not set." + ) + + configs.append( + { + "name": "tool-tavily-search", + "params": StdioServerParameters( + command=sys.executable, + args=[ + "-m", + "miroflow_tools.mcp_servers.tavily_mcp_server", + ], + env={ + "TAVILY_API_KEY": TAVILY_API_KEY, + }, + ), + } + ) + if agent_cfg.get("tools", None) is not None and "tool-python" in agent_cfg["tools"]: configs.append( { @@ -460,6 +488,7 @@ def get_env_info(cfg: DictConfig) -> dict: else {} ), # API Keys (masked for security) + "has_tavily_api_key": bool(TAVILY_API_KEY), "has_serper_api_key": bool(SERPER_API_KEY), "has_jina_api_key": bool(JINA_API_KEY), "has_anthropic_api_key": bool(ANTHROPIC_API_KEY), diff --git a/libs/miroflow-tools/pyproject.toml b/libs/miroflow-tools/pyproject.toml index 4213ce39..ea281be1 100644 --- a/libs/miroflow-tools/pyproject.toml +++ b/libs/miroflow-tools/pyproject.toml @@ -18,7 +18,8 @@ dependencies = [ "markitdown-mcp>=0.0.1a3", "google-genai", "aiohttp", - "redis" + "redis", + "tavily-python>=0.5.0" ] [build-system] diff --git a/libs/miroflow-tools/src/miroflow_tools/mcp_servers/tavily_mcp_server.py b/libs/miroflow-tools/src/miroflow_tools/mcp_servers/tavily_mcp_server.py new file mode 100644 index 00000000..35d282d1 --- /dev/null +++ b/libs/miroflow-tools/src/miroflow_tools/mcp_servers/tavily_mcp_server.py @@ -0,0 +1,95 @@ +# Copyright (c) 2025 MiroMind +# This source code is licensed under the Apache 2.0 License. + +""" +MCP server for Tavily web search. + +Provides a tool-tavily-search MCP tool that uses the Tavily API +to perform web searches optimized for LLM consumption. +""" + +import json +import os + +from mcp.server.fastmcp import FastMCP +from tavily import TavilyClient + +TAVILY_API_KEY = os.getenv("TAVILY_API_KEY", "") + +# Initialize FastMCP server +mcp = FastMCP("tavily-mcp-server") + + +@mcp.tool() +def tavily_search( + query: str, + max_results: int = 10, + search_depth: str = "basic", + topic: str = "general", + include_domains: list[str] | None = None, + exclude_domains: list[str] | None = None, + time_range: str | None = None, +) -> str: + """Perform web searches via Tavily API and retrieve results optimized for LLMs. + + Args: + query: Search query string (keep under 400 characters for best results). + max_results: Maximum number of results to return (default: 10). + search_depth: Search depth - 'basic' (fast, 1 credit) or 'advanced' (thorough, 2 credits). Default is 'basic'. + topic: Search topic category - 'general', 'news', or 'finance'. Default is 'general'. + include_domains: Optional list of domains to restrict search to (e.g., ['wikipedia.org', 'arxiv.org']). + exclude_domains: Optional list of domains to exclude from search results. + time_range: Optional time filter - 'day', 'week', 'month', or 'year'. + + Returns: + JSON string containing search results with title, url, content, and relevance score. + """ + if not TAVILY_API_KEY: + return json.dumps( + { + "success": False, + "error": "TAVILY_API_KEY environment variable not set", + "results": [], + }, + ensure_ascii=False, + ) + + if not query or not query.strip(): + return json.dumps( + { + "success": False, + "error": "Search query is required and cannot be empty", + "results": [], + }, + ensure_ascii=False, + ) + + try: + client = TavilyClient(api_key=TAVILY_API_KEY) + + kwargs = { + "query": query.strip(), + "max_results": max_results, + "search_depth": search_depth, + "topic": topic, + } + if include_domains: + kwargs["include_domains"] = include_domains + if exclude_domains: + kwargs["exclude_domains"] = exclude_domains + if time_range: + kwargs["time_range"] = time_range + + response = client.search(**kwargs) + + return json.dumps(response, ensure_ascii=False) + + except Exception as e: + return json.dumps( + {"success": False, "error": f"Unexpected error: {str(e)}", "results": []}, + ensure_ascii=False, + ) + + +if __name__ == "__main__": + mcp.run()