#
tokens: 4134/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── build
│   ├── index.d.ts
│   └── index.js
├── data
│   └── .gitkeep
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/data/.gitkeep:
--------------------------------------------------------------------------------

```

```

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

```
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Build output
build/
dist/
*.tsbuildinfo

# Environment variables
.env
.env.local
.env.*.local

# IDE and editor files
.idea/
.vscode/
*.swp
*.swo
.DS_Store

# Logs
logs/
*.log

# Testing
coverage/

# Misc
.tmp/
.temp/

# Data storage
data/*.json
# Keep the data directory but ignore its contents
!data/.gitkeep

```

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

```markdown
# Tavily MCP Server

A Model Context Protocol (MCP) server that provides AI-powered search capabilities using the Tavily API. This server enables AI assistants to perform comprehensive web searches and retrieve relevant, up-to-date information.

## Features

- AI-powered search functionality
- Support for basic and advanced search depths
- Rich search results including titles, URLs, and content snippets
- AI-generated summaries of search results
- Result scoring and response time tracking
- Comprehensive search history storage with caching
- MCP Resources for flexible data access

## Prerequisites

- Node.js (v16 or higher)
- npm (Node Package Manager)
- Tavily API key (Get one at [Tavily's website](https://tavily.com))
- An MCP client (e.g., Cline, Claude Desktop, or your own implementation)

## Installation

1. Clone the repository:
```bash
git clone https://github.com/it-beard/tavily-server.git
cd tavily-mcp-server
```

2. Install dependencies:
```bash
npm install
```

3. Build the project:
```bash
npm run build
```

## Configuration

This server can be used with any MCP client. Below are configuration instructions for popular clients:

### Cline Configuration

If you're using Cline (the VSCode extension for Claude), create or modify the MCP settings file at:
- macOS: `~/Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
- Windows: `%APPDATA%\Cursor\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json`
- Linux: `~/.config/Cursor/User/globalStorage/saoudrizwan.claude-dev\settings\cline_mcp_settings.json`

Add the following configuration (replace paths and API key with your own):
```json
{
  "mcpServers": {
    "tavily": {
      "command": "node",
      "args": ["/path/to/tavily-server/build/index.js"],
      "env": {
        "TAVILY_API_KEY": "your-api-key-here"
      }
    }
  }
}
```

### Claude Desktop Configuration

If you're using the Claude Desktop app, modify the configuration file at:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
- Linux: `~/.config/Claude/claude_desktop_config.json`

Use the same configuration format as shown above.

### Other MCP Clients

For other MCP clients, consult their documentation for the correct configuration file location and format. The server configuration should include:
1. Command to run the server (typically `node`)
2. Path to the compiled server file
3. Environment variables including the Tavily API key

## Usage

### Tools

The server provides a single tool named `search` with the following parameters:

#### Required Parameters
- `query` (string): The search query to execute

#### Optional Parameters
- `search_depth` (string): Either "basic" (faster) or "advanced" (more comprehensive)

#### Example Usage

```typescript
// Example using the MCP SDK
const result = await mcpClient.callTool("tavily", "search", {
  query: "latest developments in artificial intelligence",
  search_depth: "basic"
});
```

### Resources

The server provides both static and dynamic resources for flexible data access:

#### Static Resources
- `tavily://last-search/result`: Returns the results of the most recent search query
  - Persisted to disk in the data directory
  - Survives server restarts
  - Returns a 'No search has been performed yet' error if no search has been done

#### Dynamic Resources (Resource Templates)
- `tavily://search/{query}`: Access search results for any query
  - Replace {query} with your URL-encoded search term
  - Example: `tavily://search/artificial%20intelligence`
  - Returns cached results if the query was previously made
  - Performs and stores new search if query hasn't been searched before
  - Returns the same format as the search tool but through a resource interface

Resources in MCP provide an alternative way to access data compared to tools:
- Tools are for executing operations (like performing a new search)
- Resources are for accessing data (like retrieving existing search results)
- Resource URIs can be stored and accessed later
- Resources support both static (fixed) and dynamic (templated) access patterns

#### Response Format

```typescript
interface SearchResponse {
  query: string;
  answer: string;
  results: Array<{
    title: string;
    url: string;
    content: string;
    score: number;
  }>;
  response_time: number;
}
```

### Persistent Storage

The server implements comprehensive persistent storage for search results:

#### Storage Location
- Data is stored in the `data` directory
- `data/searches.json` contains all historical search results
- Data persists between server restarts
- Storage is automatically initialized on server start

#### Storage Features
- Stores complete search history
- Caches all search results for quick retrieval
- Automatic saving of new search results
- Disk-based persistence
- JSON format for easy debugging
- Error handling for storage operations
- Automatic directory creation

#### Caching Behavior
- All search results are cached automatically
- Subsequent requests for the same query return cached results
- Caching improves response time and reduces API calls
- Cache persists between server restarts
- Last search is tracked for quick access

## Development

### Project Structure

```
tavily-server/
├── src/
│   └── index.ts    # Main server implementation
├── data/           # Persistent storage directory
│   └── searches.json  # Search history and cache storage
├── build/          # Compiled JavaScript files
├── package.json    # Project dependencies and scripts
└── tsconfig.json   # TypeScript configuration
```

### Available Scripts

- `npm run build`: Compile TypeScript and make the output executable
- `npm run start`: Start the MCP server (after building)
- `npm run dev`: Run the server in development mode

## Error Handling

The server provides detailed error messages for common issues:
- Invalid API key
- Network errors
- Invalid search parameters
- API rate limiting
- Resource not found
- Invalid resource URIs
- Storage read/write errors

## Contributing

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Acknowledgments

- [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol/protocol) for the server framework
- [Tavily API](https://tavily.com) for providing the search capabilities

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "type": "module",
  "scripts": {
    "build": "tsc && chmod +x build/index.js"
  },
  "devDependencies": {
    "@types/node": "^22.10.2",
    "typescript": "^5.3.3"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.3",
    "axios": "^1.7.9"
  }
}

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "build",
    "rootDir": "src",
    "declaration": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build"]
}

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ErrorCode,
  ListToolsRequestSchema,
  McpError,
  ListResourcesRequestSchema,
  ListResourceTemplatesRequestSchema,
  ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import axios from 'axios';
import { mkdir, writeFile, readFile } from 'fs/promises';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

const API_KEY = process.env.TAVILY_API_KEY;
if (!API_KEY) {
  throw new Error('TAVILY_API_KEY environment variable is required');
}

// Get the directory where the script is located
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

interface TavilySearchResponse {
  results: Array<{
    title: string;
    url: string;
    content: string;
  }>;
  query: string;
}

interface TavilyErrorResponse {
  message: string;
  status?: number;
  error?: string;
}

interface StoredSearches {
  searches: { [query: string]: TavilySearchResponse };
  lastQuery: string | null;
}

const isValidSearchArgs = (
  args: any
): args is { query: string; search_depth?: 'basic' | 'advanced' } =>
  typeof args === 'object' &&
  args !== null &&
  typeof args.query === 'string' &&
  (args.search_depth === undefined ||
    args.search_depth === 'basic' ||
    args.search_depth === 'advanced');

class TavilyServer {
  private server: Server;
  private axiosInstance;
  private searches: StoredSearches = { searches: {}, lastQuery: null };
  private dataDir: string;
  private storageFile: string;

  constructor() {
    this.server = new Server(
      {
        name: 'tavily-search-server',
        version: '0.1.0',
      },
      {
        capabilities: {
          tools: {},
          resources: {},
        },
      }
    );

    this.axiosInstance = axios.create({
      baseURL: 'https://api.tavily.com',
      headers: {
        'Content-Type': 'application/json',
        'api-key': API_KEY,
      },
    });

    // Set up data storage paths
    this.dataDir = join(__dirname, '..', 'data');
    this.storageFile = join(this.dataDir, 'searches.json');

    this.setupToolHandlers();
    this.setupResourceHandlers();
    
    // Error handling
    this.server.onerror = (error) => console.error('[MCP Error]', error);
    process.on('SIGINT', async () => {
      await this.server.close();
      process.exit(0);
    });
  }

  private async initializeStorage() {
    try {
      // Create data directory if it doesn't exist
      await mkdir(this.dataDir, { recursive: true });
      
      // Try to load existing data
      try {
        const data = await readFile(this.storageFile, 'utf-8');
        this.searches = JSON.parse(data);
      } catch (error) {
        // File doesn't exist or is invalid, initialize with empty state
        this.searches = { searches: {}, lastQuery: null };
        await this.saveSearches();
      }
    } catch (error) {
      console.error('Failed to initialize storage:', error);
      throw new Error('Failed to initialize storage');
    }
  }

  private async saveSearches() {
    try {
      await writeFile(this.storageFile, JSON.stringify(this.searches, null, 2), 'utf-8');
    } catch (error) {
      console.error('Failed to save searches:', error);
      throw new Error('Failed to save searches');
    }
  }

  private async saveSearch(query: string, result: TavilySearchResponse) {
    this.searches.searches[query] = result;
    this.searches.lastQuery = query;
    await this.saveSearches();
  }

  private setupResourceHandlers() {
    // List available static resources
    this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
      resources: [
        {
          uri: 'tavily://last-search/result',
          name: 'Last Search Result',
          description: 'Results from the most recent search query',
          mimeType: 'application/json',
        }
      ],
    }));

    // List resource templates for dynamic resources
    this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
      resourceTemplates: [
        {
          uriTemplate: 'tavily://search/{query}',
          name: 'Search Results by Query',
          description: 'Search results for a specific query',
          mimeType: 'application/json',
        },
      ],
    }));

    // Handle resource reading
    this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
      // Handle static resource: last search result
      if (request.params.uri === 'tavily://last-search/result') {
        if (!this.searches.lastQuery || !this.searches.searches[this.searches.lastQuery]) {
          throw new McpError(
            ErrorCode.InvalidRequest,
            'No search has been performed yet'
          );
        }
        return {
          contents: [
            {
              uri: request.params.uri,
              mimeType: 'application/json',
              text: JSON.stringify(this.searches.searches[this.searches.lastQuery], null, 2),
            },
          ],
        };
      }

      // Handle dynamic resource: search by query
      const searchMatch = request.params.uri.match(/^tavily:\/\/search\/(.+)$/);
      if (searchMatch) {
        const query = decodeURIComponent(searchMatch[1]);
        
        // First check if we already have this search stored
        if (this.searches.searches[query]) {
          return {
            contents: [
              {
                uri: request.params.uri,
                mimeType: 'application/json',
                text: JSON.stringify(this.searches.searches[query], null, 2),
              },
            ],
          };
        }

        // If not found in storage, perform new search
        try {
          const response = await this.axiosInstance.post<TavilySearchResponse>(
            '/search',
            {
              api_key: API_KEY,
              query,
              search_depth: 'basic',
              include_answer: true,
              include_raw_content: false
            }
          );

          // Save the result
          await this.saveSearch(query, response.data);

          return {
            contents: [
              {
                uri: request.params.uri,
                mimeType: 'application/json',
                text: JSON.stringify(response.data, null, 2),
              },
            ],
          };
        } catch (error) {
          const axiosError = error as { response?: { data?: TavilyErrorResponse; status?: number }; message?: string };
          throw new McpError(
            ErrorCode.InternalError,
            `Search failed: ${axiosError.response?.data?.message ?? axiosError.message}`
          );
        }
      }

      throw new McpError(
        ErrorCode.InvalidRequest,
        `Invalid resource URI: ${request.params.uri}`
      );
    });
  }

  private setupToolHandlers() {
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: 'search',
          description: 'Perform an AI-powered search using Tavily API',
          inputSchema: {
            type: 'object',
            properties: {
              query: {
                type: 'string',
                description: 'Search query',
              },
              search_depth: {
                type: 'string',
                enum: ['basic', 'advanced'],
                description: 'Search depth - basic is faster, advanced is more comprehensive',
              },
            },
            required: ['query'],
          },
        },
      ],
    }));

    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      if (request.params.name !== 'search') {
        throw new McpError(
          ErrorCode.MethodNotFound,
          `Unknown tool: ${request.params.name}`
        );
      }

      if (!isValidSearchArgs(request.params.arguments)) {
        throw new McpError(
          ErrorCode.InvalidParams,
          'Invalid search arguments'
        );
      }

      try {
        console.error('Making request to Tavily API...'); // Debug log
        const response = await this.axiosInstance.post<TavilySearchResponse>(
          '/search',
          {
            api_key: API_KEY,
            query: request.params.arguments.query,
            search_depth: request.params.arguments.search_depth || 'basic',
            include_answer: true,
            include_raw_content: false
          }
        );

        // Save the result
        await this.saveSearch(request.params.arguments.query, response.data);

        console.error('Received response from Tavily API'); // Debug log
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify(response.data, null, 2),
            },
          ],
        };
      } catch (error) {
        console.error('Tavily API Error:', error); // Debug log
        const axiosError = error as { response?: { data?: TavilyErrorResponse; status?: number }; message?: string };
        const errorMessage = axiosError.response?.data?.message ?? 
                           axiosError.response?.data?.error ??
                           axiosError.message ??
                           'Unknown error occurred';
        const statusCode = axiosError.response?.status ?? 'unknown';
        
        console.error(`Error details - Message: ${errorMessage}, Status: ${statusCode}`); // Debug log
        
        return {
          content: [
            {
              type: 'text',
              text: `Tavily API error: ${errorMessage} (Status: ${statusCode})`,
            },
          ],
          isError: true,
        };
      }
    });
  }

  async run() {
    // Initialize storage before starting the server
    await this.initializeStorage();
    
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error('Tavily MCP server running on stdio');
  }
}

const server = new TavilyServer();
server.run().catch(console.error);

```