# Directory Structure ``` ├── .gitignore ├── .python-version ├── docker-compose.yml ├── Dockerfile ├── LICENSE ├── pyproject.toml ├── README_JP.md ├── README.md ├── smithery.yaml ├── src │ ├── __init__.py │ ├── client.py │ ├── server.py │ └── taivily_client.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 1 | 3.13 2 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # Virtual environments 10 | .venv 11 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # tavily-search MCP server 2 | 3 | A MCP server project 4 | 5 | [](https://archestra.ai/mcp-catalog/tomatio13__mcp-server-tavily) 6 | <a href="https://glama.ai/mcp/servers/s0hka6zney"><img width="380" height="200" src="https://glama.ai/mcp/servers/s0hka6zney/badge" alt="tavily-search MCP server" /></a> 7 | 8 | ## Components 9 | 10 | This server uses the Tavily API to perform searches based on specified queries. 11 | - Search results are returned in text format. 12 | - Search results include AI responses, URIs, and titles of the search results. 13 | 14 | ### Tools 15 | 16 | This server implements the following tools: 17 | - search: Performs searches based on specified queries 18 | - Required argument: "query" 19 | - Optional argument: "search_depth" (basic or advanced) 20 | 21 | ### Installing via Smithery 22 | 23 | To install Tavily Search for Claude Desktop automatically via [Smithery](https://smithery.ai/server/tavily-search): 24 | 25 | ```bash 26 | npx -y @smithery/cli install tavily-search --client claude 27 | ``` 28 | 29 | ### Install 30 | 31 | 1. Download the repository. 32 | ```bash 33 | git clone https://github.com/Tomatio13/mcp-server-tavily.git 34 | ``` 35 | 2. Open the Claude Desktop configuration file. 36 | ``` 37 | On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json` 38 | On Windows: `C:\Users\[username]\AppData\Roaming\Claude\claude_desktop_config.json` 39 | ``` 40 | 41 | 3. Edit the configuration file as follows: 42 | ```yaml 43 | "mcpServers": { 44 | "tavily-search": { 45 | "command": "uv", 46 | "args": [ 47 | "--directory", 48 | "C:\\your_path\\mcp-server-tavily", 49 | "run", 50 | "tavily-search" 51 | ], 52 | "env": { 53 | "TAVILY_API_KEY": "YOUR_TAVILY_API_KEY", 54 | "PYTHONIOENCODING": "utf-8" 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | 4. Restart Claude Desktop. 61 | 62 | ## Usage 63 | 64 | In Claude Desktop, when you ask "Please search for something", you will receive search results. 65 | 66 | Search example: 67 | ``` 68 | Please search in detail for today's events in Kamakura 69 | ``` 70 | Response example: 71 | ``` 72 | According to the search results, the following events start today, December 1st: 73 | "Kamakura Promotion Photo Contest 2025" 74 | Period: December 1, 2024 - January 31, 2025 75 | A photo contest for those who love Kamakura 76 | Applications start accepting from today 77 | Also, as a related upcoming event: 78 | On December 7th, an exhibition by 12 Kamakura artists will be held at the Seibu Press Inn Kamakura Ofuna Station East Exit Lounge. 79 | ``` 80 | 81 | ## Log Storage Location 82 | 83 | Logs are stored in the following location: 84 | 85 | For Windows: 86 | ``` 87 | C:\Users\[username]\AppData\Roaming\Claude\logs\mcp-server-tavily-search 88 | ``` 89 | ## Execution with Cursor 90 | 91 | 1. Create a shell script (e.g., `script.sh`) as shown below: 92 | 93 | ```bash 94 | #!/bin/bash 95 | TARGET_DIR=/path/to/mcp-server-tavily 96 | cd "${TARGET_DIR}" 97 | export TAVILY_API_KEY="your-api-key" 98 | export PYTHONIOENCODING=utf-8 99 | uv --directory $PWD run tavily-search 100 | ``` 101 | 102 | 2. Configure Cursor's MCP Server settings as follows: 103 | 104 | ``` 105 | Name: tavily-search 106 | Type: command 107 | Command: /path/to/your/script.sh 108 | ``` 109 | 110 | 3. Save the settings. 111 | 112 | 4. Once the settings are saved, you can ask Cursor's Composer-Agent to "search for something," and it will return the search results. 113 | 114 | ## Running in Local Environment Using Docker Compose 115 | 116 | ### Purpose 117 | For operating systems other than Windows/MacOS where Claude Desktop cannot be used, 118 | this section explains how to set up and run an MCP server and client in a local environment 119 | using Docker compose. 120 | 121 | ### Steps 122 | 1. Install Docker. 123 | 2. Download the repository. 124 | ```bash 125 | git clone https://github.com/Tomatio13/mcp-server-tavily.git 126 | ``` 127 | 3. Run Docker compose. 128 | ```bash 129 | docker compose up -d 130 | ``` 131 | 4. Execute the client. 132 | ```bash 133 | docker exec mcp_server uv --directory /usr/src/app/mcp-server-tavily/src run client.py 134 | ``` 135 | 5. Execution Results 136 | 6. After searching for available tools as shown below, a query will be issued to Tavily and a response will be returned: 137 | ```bash 138 | 2024-12-01 11:21:56,930 - tavily-search-server - INFO - Starting Tavily search server 139 | 2024-12-01 11:21:56,932 - tavily-search-server - INFO - Server initialized, starting main loop 140 | 2024-12-01 11:21:56,936 - mcp.server - INFO - Processing request of type ListToolsRequest 141 | 2024-12-01 11:21:56,936 - tavily-search-server - INFO - Listing available tools 142 | 利用可能なツール: nextCursor=None tools=[Tool(name='search', description='Search the web using Tavily API', inputSchema={'type': 'object', 'properties': {'query': {'type': 'string', 'description': 'Search query'}, 'search_depth': {'type': 'string', 'description': 'Search depth (basic or advanced)', 'enum': ['basic', 'advanced']}}, 'required': ['query']})] 143 | 2024-12-01 11:21:56,937 - mcp.server - INFO - Processing request of type CallToolRequest 144 | 2024-12-01 11:21:56,937 - tavily-search-server - INFO - TOOL_CALL_DEBUG: Tool called - name: search, arguments: {'query': '今日の東京タワーのイベントを教えて下さい'} 145 | 2024-12-01 11:21:56,937 - tavily-search-server - INFO - Executing search with query: '今日の東京タワーのイベントを教えて下さい' 146 | 2024-12-01 11:22:00,243 - httpx - INFO - HTTP Request: POST https://api.tavily.com/search "HTTP/1.1 200 OK" 147 | 2024-12-01 11:22:00,243 - tavily-search-server - INFO - Search successful - Answer generated 148 | 2024-12-01 11:22:00,243 - tavily-search-server - INFO - Search successful - Results available 149 | ツール実行結果: content=[TextContent(type='text', text='AI Answer:\n今日の東京タワーのイベントは以下の通りです:\n1. Candlelight: エド・シーランとコールドプレイのヒットメドレー - 12月01日\n2. チームラボプラネッツ TOKYO - 12月01日から1月21日\n\n他にもイベントがある可能性がありますので、公式ウェブサイト等で最新情報をご確認ください。\n\n\n\nSearch Results:\n\n1. 東京タワー (東京): 現在のイベントとチケット | Fever\nURL: https://feverup.com/ja/tokyo/venue/tokyo-tower\nSummary: Summary not found\n\n\n2. 東京タワー(東京都)の施設で開催するイベント一覧|ウォーカープラス\nURL: https://www.walkerplus.com/spot/ar0313s03867/e_list.html\nSummary: Summary not found\n\n\n3. 東京タワー - Tokyo Tower\nURL: https://www.tokyotower.co.jp/event/\nSummary: Summary not found\n')] isError=False 150 | ``` 151 | ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- ```yaml 1 | services: 2 | app: 3 | image: ghcr.io/astral-sh/uv:python3.12-bookworm 4 | container_name: mcp_server 5 | volumes: 6 | - .:/usr/src/app/mcp-server-tavily 7 | working_dir: /usr/src/app 8 | command: tail -f /dev/null 9 | environment: 10 | - PYTHONUNBUFFERED=1 11 | - TAVILY_API_KEY=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 12 | extra_hosts: 13 | - "host.docker.internal:host-gateway" 14 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "tavily-search" 3 | version = "0.1.0" 4 | description = "A MCP server project" 5 | readme = "README.md" 6 | requires-python = ">=3.13" 7 | dependencies = [ 8 | "httpx>=0.28.0", 9 | "mcp>=1.0.0", 10 | "python-dotenv>=1.0.1", 11 | "tavily-python>=0.5.0", 12 | ] 13 | 14 | [build-system] 15 | requires = [ "hatchling",] 16 | build-backend = "hatchling.build" 17 | 18 | [project.scripts] 19 | tavily-search = "src:main" 20 | 21 | [tool.hatch.build.targets.wheel] 22 | packages = ["src"] 23 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - tavilyApiKey 10 | properties: 11 | tavilyApiKey: 12 | type: string 13 | description: The API key for the Tavily Search server. 14 | commandFunction: 15 | # A function that produces the CLI command to start the MCP on stdio. 16 | |- 17 | (config) => ({ command: 'uv', args: ['--directory', '/app/src', 'run', 'server.py'], env: { TAVILY_API_KEY: config.tavilyApiKey } }) 18 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | # Use a Python image with uv pre-installed 3 | FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv 4 | 5 | # Install the project into /app 6 | WORKDIR /app 7 | 8 | # Enable bytecode compilation 9 | ENV UV_COMPILE_BYTECODE=1 10 | 11 | # Copy from the cache instead of linking since it's a mounted volume 12 | ENV UV_LINK_MODE=copy 13 | 14 | # Install the project's dependencies using the lockfile and settings 15 | COPY pyproject.toml uv.lock ./ 16 | COPY README.md uv.lock ./ 17 | RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-install-project --no-dev --no-editable 18 | 19 | # Then, add the rest of the project source code and install it 20 | # Installing separately from its dependencies allows optimal layer caching 21 | COPY src/ /app/src/ 22 | RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev --no-editable 23 | 24 | FROM python:3.12-slim-bookworm 25 | 26 | WORKDIR /app 27 | 28 | COPY --from=uv /root/.local /root/.local 29 | COPY --from=uv --chown=app:app /app/.venv /app/.venv 30 | 31 | # Place executables in the environment at the front of the path 32 | ENV PATH="/app/.venv/bin:$PATH" 33 | 34 | # Set necessary environment variables 35 | ENV TAVILY_API_KEY=your_api_key_here 36 | 37 | ENTRYPOINT ["uv", "--directory", "/app/src", "run", "server.py"] 38 | ``` -------------------------------------------------------------------------------- /src/server.py: -------------------------------------------------------------------------------- ```python 1 | import os 2 | import json 3 | import logging 4 | from datetime import datetime 5 | from collections.abc import Sequence 6 | from typing import Any, Optional 7 | from tavily import AsyncTavilyClient 8 | from dotenv import load_dotenv 9 | from mcp.server import Server 10 | from mcp.types import ( 11 | Resource, 12 | Tool, 13 | TextContent, 14 | ImageContent, 15 | EmbeddedResource, 16 | EmptyResult 17 | ) 18 | from pydantic import AnyUrl 19 | import asyncio 20 | 21 | # 環境変数の読み込み 22 | load_dotenv() 23 | 24 | # ログの準備 25 | logging.basicConfig( 26 | level=logging.INFO, 27 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 28 | ) 29 | logger = logging.getLogger("tavily-search-server") 30 | 31 | # APIキーの準備 32 | API_KEY = os.getenv("TAVILY_API_KEY") 33 | if not API_KEY: 34 | logger.error("TAVILY_API_KEY environment variable not found") 35 | raise ValueError("TAVILY_API_KEY environment variable required") 36 | 37 | # サーバの準備 38 | app = Server("tavily-search-server") 39 | 40 | @app.list_resources() 41 | async def list_resources() -> list[Resource]: 42 | logger.info("Listing available resources") 43 | resources = [ 44 | Resource( 45 | uri=AnyUrl(f"websearch://query=`who is current Prime Minister of Japan 2024`,search_depth=`basic`"), 46 | name="Web Search about `who is current Prime Minister of Japan 2024`.\ 47 | There are two types of search_depth: 'basic' and 'advanced', with 'advanced' searching deeper.'", 48 | mimeType="application/json", 49 | description="General web search using Tavily API" 50 | ) 51 | ] 52 | logger.debug(f"Returning resources with full content: {resources}") 53 | return resources 54 | 55 | 56 | # 利用可能なツール一覧の取得 57 | @app.list_tools() 58 | async def list_tools() -> list[Tool]: 59 | """利用可能なツールの一覧を返す""" 60 | logger.info("Listing available tools") 61 | tools = [ 62 | Tool( 63 | name="search", 64 | description="Search the web using Tavily API", 65 | inputSchema={ 66 | "type": "object", 67 | "properties": { 68 | "query": { 69 | "type": "string", 70 | "description": "Search query" 71 | }, 72 | "search_depth": { 73 | "type": "string", 74 | "description": "Search depth (basic or advanced)", 75 | "enum": ["basic", "advanced"] 76 | } 77 | }, 78 | "required": ["query"] 79 | } 80 | ) 81 | ] 82 | logger.debug(f"Returning tools: {tools}") 83 | return tools 84 | 85 | # 検索結果を処理する関数 86 | async def process_search_results(results: dict) -> TextContent: 87 | """検索結果を処理してTextContentを返す""" 88 | if not results: 89 | logger.warning("Empty search results received") 90 | return TextContent( 91 | type="text", 92 | text="No results were found for your query. Please try a different search term." 93 | ) 94 | 95 | # 結果を格納する文字列を作成 96 | response_text = [] 97 | 98 | # AI回答を追加 99 | if 'answer' in results and results['answer']: 100 | logger.info("Search successful - Answer generated") 101 | response_text.append("AI Answer:") 102 | response_text.append(results['answer']) 103 | response_text.append("\n") 104 | 105 | # 検索結果を追加 106 | if 'results' in results and results['results']: 107 | logger.info("Search successful - Results available") 108 | response_text.append("\nSearch Results:") 109 | for i, result in enumerate(results['results'], 1): 110 | response_text.append(f"\n{i}. {result.get('title', 'Title not found')}") 111 | response_text.append(f"URL: {result.get('url', 'URL not found')}") 112 | response_text.append(f"Summary: {result.get('snippet', 'Summary not found')}\n") 113 | 114 | if response_text: 115 | return TextContent(type="text", text="\n".join(response_text)) 116 | 117 | logger.warning("No answer or results found in search results") 118 | return TextContent( 119 | type="text", 120 | text="The search was completed but no relevant information was found. Please try refining your query." 121 | ) 122 | 123 | # ツールの呼び出し 124 | @app.call_tool() 125 | async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]: 126 | """ツールを呼び出して結果を返す""" 127 | logger.info(f"TOOL_CALL_DEBUG: Tool called - name: {name}, arguments: {arguments}") 128 | 129 | if name != "search": 130 | logger.error(f"Unknown tool requested: {name}") 131 | return [TextContent( 132 | type="text", 133 | text=f"Error: Unknown tool '{name}'. Only 'search' is supported." 134 | )] 135 | 136 | if not isinstance(arguments, dict) or "query" not in arguments: 137 | logger.error(f"Invalid arguments provided: {arguments}") 138 | return [TextContent( 139 | type="text", 140 | text="Error: Invalid arguments. A 'query' parameter is required." 141 | )] 142 | 143 | try: 144 | client = AsyncTavilyClient(API_KEY) 145 | query = arguments["query"] 146 | 147 | logger.info(f"Executing search with query: '{query}'") 148 | 149 | search_task = client.search( 150 | query=query, 151 | search_depth=arguments.get("search_depth", "basic"), 152 | include_images=False, 153 | include_answer=True, 154 | max_results=3, 155 | topic="general" 156 | ) 157 | 158 | try: 159 | # タイムアウトを設定して検索を実行 160 | results = await asyncio.wait_for(search_task, timeout=30.0) 161 | logger.debug(f"Raw search results: {results}") 162 | 163 | # 結果の処理 164 | return [await process_search_results(results)] 165 | 166 | except asyncio.TimeoutError: 167 | logger.error("Search operation timed out after 30 seconds") 168 | return [TextContent( 169 | type="text", 170 | text="The search operation timed out. Please try again with a more specific query or check your internet connection." 171 | )] 172 | 173 | except Exception as e: 174 | error_message = str(e) 175 | logger.error(f"Search failed: {error_message}", exc_info=True) 176 | 177 | # エラーメッセージをユーザーフレンドリー形式に変換 178 | if "api_key" in error_message.lower(): 179 | return [TextContent( 180 | type="text", 181 | text="Authentication error occurred. Please check the API key configuration." 182 | )] 183 | elif "rate limit" in error_message.lower(): 184 | return [TextContent( 185 | type="text", 186 | text="Rate limit exceeded. Please wait a moment before trying again." 187 | )] 188 | else: 189 | return [TextContent( 190 | type="text", 191 | text=f"An unexpected error occurred during the search. Please try again later. Error: {error_message}" 192 | )] 193 | 194 | # メイン実行部分 195 | async def main(): 196 | logger.info("Starting Tavily search server") 197 | try: 198 | from mcp.server.stdio import stdio_server 199 | 200 | async with stdio_server() as (read_stream, write_stream): 201 | logger.info("Server initialized, starting main loop") 202 | await app.run( 203 | read_stream, 204 | write_stream, 205 | app.create_initialization_options() 206 | ) 207 | 208 | except Exception as e: 209 | logger.error(f"Server failed to start: {str(e)}", exc_info=True) 210 | raise 211 | 212 | # エントリーポイント追加 213 | if __name__ == "__main__": 214 | main_entry() 215 | 216 | def main_entry(): 217 | try: 218 | asyncio.run(main()) 219 | except KeyboardInterrupt: 220 | logger.info("Server shutdown requested") 221 | except Exception as e: 222 | logger.error(f"Server error: {e}", exc_info=True) 223 | raise 224 | ```