#
tokens: 4508/50000 8/8 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
3.13

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# tavily-search MCP server

A MCP server project

[![Trust Score](https://archestra.ai/mcp-catalog/api/badge/quality/Tomatio13/mcp-server-tavily)](https://archestra.ai/mcp-catalog/tomatio13__mcp-server-tavily)
<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>

## Components

This server uses the Tavily API to perform searches based on specified queries.
- Search results are returned in text format.
- Search results include AI responses, URIs, and titles of the search results.

### Tools

This server implements the following tools:
- search: Performs searches based on specified queries
  - Required argument: "query"
  - Optional argument: "search_depth" (basic or advanced)

### Installing via Smithery

To install Tavily Search for Claude Desktop automatically via [Smithery](https://smithery.ai/server/tavily-search):

```bash
npx -y @smithery/cli install tavily-search --client claude
```

### Install

1. Download the repository.
```bash
git clone https://github.com/Tomatio13/mcp-server-tavily.git
``` 
2. Open the Claude Desktop configuration file.
```
On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
On Windows: `C:\Users\[username]\AppData\Roaming\Claude\claude_desktop_config.json`
```

3. Edit the configuration file as follows:
  ```yaml
  "mcpServers": {
    "tavily-search": {
      "command": "uv",
      "args": [
        "--directory",
        "C:\\your_path\\mcp-server-tavily",
        "run",
        "tavily-search"
      ],
      "env": {
        "TAVILY_API_KEY": "YOUR_TAVILY_API_KEY",
        "PYTHONIOENCODING": "utf-8"
      }
    }
  }
  ```

4. Restart Claude Desktop.

## Usage

In Claude Desktop, when you ask "Please search for something", you will receive search results.

Search example:
```
Please search in detail for today's events in Kamakura
```
Response example:
```
According to the search results, the following events start today, December 1st:
"Kamakura Promotion Photo Contest 2025"
Period: December 1, 2024 - January 31, 2025
A photo contest for those who love Kamakura
Applications start accepting from today
Also, as a related upcoming event:
On December 7th, an exhibition by 12 Kamakura artists will be held at the Seibu Press Inn Kamakura Ofuna Station East Exit Lounge.
```

## Log Storage Location

Logs are stored in the following location:

For Windows:
```
C:\Users\[username]\AppData\Roaming\Claude\logs\mcp-server-tavily-search
```
## Execution with Cursor

1. Create a shell script (e.g., `script.sh`) as shown below:

```bash
#!/bin/bash
TARGET_DIR=/path/to/mcp-server-tavily
cd "${TARGET_DIR}"
export TAVILY_API_KEY="your-api-key"
export PYTHONIOENCODING=utf-8
uv --directory $PWD run tavily-search
```

2. Configure Cursor's MCP Server settings as follows:

```
Name: tavily-search
Type: command
Command: /path/to/your/script.sh
```

3. Save the settings.

4. Once the settings are saved, you can ask Cursor's Composer-Agent to "search for something," and it will return the search results.

## Running in Local Environment Using Docker Compose

### Purpose
For operating systems other than Windows/MacOS where Claude Desktop cannot be used,
this section explains how to set up and run an MCP server and client in a local environment
using Docker compose.

### Steps
1. Install Docker.
2. Download the repository.
```bash
git clone https://github.com/Tomatio13/mcp-server-tavily.git
``` 
3. Run Docker compose.
```bash
docker compose up -d
``` 
4. Execute the client.
```bash
docker exec mcp_server uv --directory /usr/src/app/mcp-server-tavily/src run client.py
```
5. Execution Results
6. After searching for available tools as shown below, a query will be issued to Tavily and a response will be returned:
```bash
2024-12-01 11:21:56,930 - tavily-search-server - INFO - Starting Tavily search server
2024-12-01 11:21:56,932 - tavily-search-server - INFO - Server initialized, starting main loop
2024-12-01 11:21:56,936 - mcp.server - INFO - Processing request of type ListToolsRequest
2024-12-01 11:21:56,936 - tavily-search-server - INFO - Listing available tools
利用可能なツール: 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']})]
2024-12-01 11:21:56,937 - mcp.server - INFO - Processing request of type CallToolRequest
2024-12-01 11:21:56,937 - tavily-search-server - INFO - TOOL_CALL_DEBUG: Tool called - name: search, arguments: {'query': '今日の東京タワーのイベントを教えて下さい'}
2024-12-01 11:21:56,937 - tavily-search-server - INFO - Executing search with query: '今日の東京タワーのイベントを教えて下さい'
2024-12-01 11:22:00,243 - httpx - INFO - HTTP Request: POST https://api.tavily.com/search "HTTP/1.1 200 OK"
2024-12-01 11:22:00,243 - tavily-search-server - INFO - Search successful - Answer generated
2024-12-01 11:22:00,243 - tavily-search-server - INFO - Search successful - Results available
ツール実行結果: 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
``` 

```

--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------

```yaml
services:
  app:
    image: ghcr.io/astral-sh/uv:python3.12-bookworm
    container_name: mcp_server
    volumes:
      - .:/usr/src/app/mcp-server-tavily
    working_dir: /usr/src/app
    command: tail -f /dev/null
    environment:
      - PYTHONUNBUFFERED=1
      - TAVILY_API_KEY=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
    extra_hosts:
      - "host.docker.internal:host-gateway"

```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "tavily-search"
version = "0.1.0"
description = "A MCP server project"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
 "httpx>=0.28.0",
 "mcp>=1.0.0",
 "python-dotenv>=1.0.1",
 "tavily-python>=0.5.0",
]

[build-system]
requires = [ "hatchling",]
build-backend = "hatchling.build"

[project.scripts]
tavily-search = "src:main"

[tool.hatch.build.targets.wheel]
packages = ["src"]

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    required:
      - tavilyApiKey
    properties:
      tavilyApiKey:
        type: string
        description: The API key for the Tavily Search server.
  commandFunction:
    # A function that produces the CLI command to start the MCP on stdio.
    |-
    (config) => ({ command: 'uv', args: ['--directory', '/app/src', 'run', 'server.py'], env: { TAVILY_API_KEY: config.tavilyApiKey } })

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
# Use a Python image with uv pre-installed
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv

# Install the project into /app
WORKDIR /app

# Enable bytecode compilation
ENV UV_COMPILE_BYTECODE=1

# Copy from the cache instead of linking since it's a mounted volume
ENV UV_LINK_MODE=copy

# Install the project's dependencies using the lockfile and settings
COPY pyproject.toml uv.lock ./
COPY README.md uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-install-project --no-dev --no-editable

# Then, add the rest of the project source code and install it
# Installing separately from its dependencies allows optimal layer caching
COPY src/ /app/src/
RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev --no-editable

FROM python:3.12-slim-bookworm

WORKDIR /app

COPY --from=uv /root/.local /root/.local
COPY --from=uv --chown=app:app /app/.venv /app/.venv

# Place executables in the environment at the front of the path
ENV PATH="/app/.venv/bin:$PATH"

# Set necessary environment variables
ENV TAVILY_API_KEY=your_api_key_here

ENTRYPOINT ["uv", "--directory", "/app/src", "run", "server.py"]

```

--------------------------------------------------------------------------------
/src/server.py:
--------------------------------------------------------------------------------

```python
import os
import json
import logging
from datetime import datetime
from collections.abc import Sequence
from typing import Any, Optional
from tavily import AsyncTavilyClient
from dotenv import load_dotenv
from mcp.server import Server
from mcp.types import (
    Resource,
    Tool,
    TextContent,
    ImageContent,
    EmbeddedResource,
    EmptyResult
)
from pydantic import AnyUrl
import asyncio

# 環境変数の読み込み
load_dotenv()

# ログの準備
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("tavily-search-server")

# APIキーの準備
API_KEY = os.getenv("TAVILY_API_KEY")
if not API_KEY:
    logger.error("TAVILY_API_KEY environment variable not found")
    raise ValueError("TAVILY_API_KEY environment variable required")

# サーバの準備
app = Server("tavily-search-server")

@app.list_resources()
async def list_resources() -> list[Resource]:
    logger.info("Listing available resources")
    resources = [
        Resource(
            uri=AnyUrl(f"websearch://query=`who is current Prime Minister of Japan 2024`,search_depth=`basic`"),
            name="Web Search about `who is current Prime Minister of Japan 2024`.\
                There are two types of search_depth: 'basic' and 'advanced', with 'advanced' searching deeper.'",
            mimeType="application/json",
            description="General web search using Tavily API"
        )
    ]
    logger.debug(f"Returning resources with full content: {resources}")
    return resources
    

# 利用可能なツール一覧の取得
@app.list_tools()
async def list_tools() -> list[Tool]:
    """利用可能なツールの一覧を返す"""
    logger.info("Listing available tools")
    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"]
            }
        )
    ]
    logger.debug(f"Returning tools: {tools}")
    return tools

# 検索結果を処理する関数
async def process_search_results(results: dict) -> TextContent:
    """検索結果を処理してTextContentを返す"""
    if not results:
        logger.warning("Empty search results received")
        return TextContent(
            type="text",
            text="No results were found for your query. Please try a different search term."
        )

    # 結果を格納する文字列を作成
    response_text = []

    # AI回答を追加
    if 'answer' in results and results['answer']:
        logger.info("Search successful - Answer generated")
        response_text.append("AI Answer:")
        response_text.append(results['answer'])
        response_text.append("\n")

    # 検索結果を追加
    if 'results' in results and results['results']:
        logger.info("Search successful - Results available")
        response_text.append("\nSearch Results:")
        for i, result in enumerate(results['results'], 1):
            response_text.append(f"\n{i}. {result.get('title', 'Title not found')}")
            response_text.append(f"URL: {result.get('url', 'URL not found')}")
            response_text.append(f"Summary: {result.get('snippet', 'Summary not found')}\n")

    if response_text:
        return TextContent(type="text", text="\n".join(response_text))
    
    logger.warning("No answer or results found in search results")
    return TextContent(
        type="text",
        text="The search was completed but no relevant information was found. Please try refining your query."
    )

# ツールの呼び出し
@app.call_tool()
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
    """ツールを呼び出して結果を返す"""
    logger.info(f"TOOL_CALL_DEBUG: Tool called - name: {name}, arguments: {arguments}")
    
    if name != "search":
        logger.error(f"Unknown tool requested: {name}")
        return [TextContent(
            type="text",
            text=f"Error: Unknown tool '{name}'. Only 'search' is supported."
        )]

    if not isinstance(arguments, dict) or "query" not in arguments:
        logger.error(f"Invalid arguments provided: {arguments}")
        return [TextContent(
            type="text",
            text="Error: Invalid arguments. A 'query' parameter is required."
        )]

    try:
        client = AsyncTavilyClient(API_KEY)
        query = arguments["query"]
        
        logger.info(f"Executing search with query: '{query}'")

        search_task = client.search(
            query=query,
            search_depth=arguments.get("search_depth", "basic"),
            include_images=False,
            include_answer=True,
            max_results=3,
            topic="general"
        )

        try:
            # タイムアウトを設定して検索を実行
            results = await asyncio.wait_for(search_task, timeout=30.0)
            logger.debug(f"Raw search results: {results}")
            
            # 結果の処理
            return [await process_search_results(results)]

        except asyncio.TimeoutError:
            logger.error("Search operation timed out after 30 seconds")
            return [TextContent(
                type="text",
                text="The search operation timed out. Please try again with a more specific query or check your internet connection."
            )]
            
    except Exception as e:
        error_message = str(e)
        logger.error(f"Search failed: {error_message}", exc_info=True)
        
        # エラーメッセージをユーザーフレンドリー形式に変換
        if "api_key" in error_message.lower():
            return [TextContent(
                type="text",
                text="Authentication error occurred. Please check the API key configuration."
            )]
        elif "rate limit" in error_message.lower():
            return [TextContent(
                type="text",
                text="Rate limit exceeded. Please wait a moment before trying again."
            )]
        else:
            return [TextContent(
                type="text",
                text=f"An unexpected error occurred during the search. Please try again later. Error: {error_message}"
            )]

# メイン実行部分
async def main():
    logger.info("Starting Tavily search server")
    try:
        from mcp.server.stdio import stdio_server

        async with stdio_server() as (read_stream, write_stream):
            logger.info("Server initialized, starting main loop")
            await app.run(
                read_stream,
                write_stream,
                app.create_initialization_options()
            )

    except Exception as e:
        logger.error(f"Server failed to start: {str(e)}", exc_info=True)
        raise

# エントリーポイント追加
if __name__ == "__main__":
    main_entry()

def main_entry():
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        logger.info("Server shutdown requested")
    except Exception as e:
        logger.error(f"Server error: {e}", exc_info=True)
        raise

```