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

```
├── .gitignore
├── Dockerfile
├── LICENSE
├── package-lock.json
├── package.json
├── readme.md
├── readme.zh-CN.md
├── smithery.yaml
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

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

```
node_modules/

dist/
```

--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------

```markdown
# MCP Tavily

[![smithery badge](https://smithery.ai/badge/@kshern/mcp-tavily)](https://smithery.ai/server/@kshern/mcp-tavily)

[中文文档](./readme.zh-CN.md)

A Model Context Protocol (MCP) server implementation for Tavily API, providing advanced search and content extraction capabilities.

## Features

- **Multiple Search Tools**:
  - `search`: Basic search functionality with customizable options
  - `searchContext`: Context-aware search for better relevance
  - `searchQNA`: Question and answer focused search
- **Content Extraction**: Extract content from URLs with configurable options
- **Rich Configuration Options**: Extensive options for search depth, filtering, and content inclusion


### Usage with MCP

Add the Tavily MCP server to your MCP configuration:

```json
{
  "mcpServers": {
    "tavily": {
      "command": "npx",
      "args": ["-y", "@mcptools/mcp-tavily"],
      "env": {
        "TAVILY_API_KEY": "your-api-key"
      }
    }
  }
}
```

> Note: Make sure to replace `your-api-key` with your actual Tavily API key. You can also set it as an environment variable `TAVILY_API_KEY` before running the server.

## API Reference

### Search Tools

The server provides three search tools that can be called through MCP:

#### 1. Basic Search
```typescript
// Tool name: search
{
  query: "artificial intelligence",
  options: {
    searchDepth: "advanced",
    topic: "news",
    maxResults: 10
  }
}
```

#### 2. Context Search
```typescript
// Tool name: searchContext
{
  query: "latest developments in AI",
  options: {
    topic: "news",
    timeRange: "week"
  }
}
```

#### 3. Q&A Search
```typescript
// Tool name: searchQNA
{
  query: "What is quantum computing?",
  options: {
    includeAnswer: true,
    maxResults: 5
  }
}
```

### Extract Tool

```typescript
// Tool name: extract
{
  urls: ["https://example.com/article1", "https://example.com/article2"],
  options: {
    extractDepth: "advanced",
    includeImages: true
  }
}
```

### Search Options

All search tools share these options:

```typescript
interface SearchOptions {
  searchDepth?: "basic" | "advanced";    // Search depth level
  topic?: "general" | "news" | "finance"; // Search topic category
  days?: number;                         // Number of days to search
  maxResults?: number;                   // Maximum number of results
  includeImages?: boolean;               // Include images in results
  includeImageDescriptions?: boolean;    // Include image descriptions
  includeAnswer?: boolean;               // Include answer in results
  includeRawContent?: boolean;           // Include raw content
  includeDomains?: string[];            // List of domains to include
  excludeDomains?: string[];            // List of domains to exclude
  maxTokens?: number;                    // Maximum number of tokens
  timeRange?: "year" | "month" | "week" | "day" | "y" | "m" | "w" | "d"; // Time range for search
}
```

### Extract Options

```typescript
interface ExtractOptions {
  extractDepth?: "basic" | "advanced";   // Extraction depth level
  includeImages?: boolean;               // Include images in results
}
```

## Response Format

All tools return responses in the following format:

```typescript
{
  content: Array<{
    type: "text",
    text: string
  }>
}
```

For search results, each item includes:
- Title
- Content
- URL

For extracted content, each item includes:
- URL
- Raw content
- Failed URLs list (if any)

## Error Handling

All tools include proper error handling and will throw descriptive error messages if something goes wrong.

## Installation

### Installing via Smithery

To install Tavily API Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@kshern/mcp-tavily):

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

### Manual Installation
```bash
npm install @mcptools/mcp-tavily
```

Or use it directly with npx:

```bash
npx @mcptools/mcp-tavily
```



### Prerequisites

- Node.js 16 or higher
- npm or yarn
- Tavily API key (get one from [Tavily](https://tavily.com))

### Setup

1. Clone the repository
2. Install dependencies:
```bash
npm install
```
3. Set your Tavily API key:
```bash
export TAVILY_API_KEY=your_api_key
```


### Building

```bash
npm run build
```

## Debugging with MCP Inspector

For development and debugging, we recommend using [MCP Inspector](https://github.com/modelcontextprotocol/inspector), a powerful development tool for MCP servers.


The Inspector provides a user interface for:
- Testing tool calls
- Viewing server responses
- Debugging tool execution
- Monitoring server state

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

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

## License

This project is licensed under the MIT License.

## Support

For any questions or issues:
- Tavily API: refer to the [Tavily documentation](https://docs.tavily.com/)
- MCP integration: refer to the [MCP documentation](https://modelcontextprotocol.io//)
```

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

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

```

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

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine

# Create app directory
WORKDIR /app

# Install app dependencies
COPY package.json package-lock.json ./
RUN npm install --ignore-scripts

# Bundle app source code
COPY . .

# Build the TypeScript code
RUN npm run build

# Expose no ports - using stdio for communication

# Start the MCP server
CMD [ "npm", "start" ]

```

--------------------------------------------------------------------------------
/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: API key for the Tavily service
  commandFunction:
    # A function that produces the CLI command to start the MCP on stdio.
    |-
    (config) => ({
      command: 'npm',
      args: ['start'],
      env: { TAVILY_API_KEY: config.tavilyApiKey }
    })
  exampleConfig:
    tavilyApiKey: your-tavily-api-key-here

```

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

```json
{
  "name": "@mcptools/mcp-tavily",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "bin": {
    "mcp-tavily": "./dist/index.js"
  },
  "files": [
    "dist",
    "README.md"
  ],
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "prepublishOnly": "npm run build",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "mcp",
    "tavily",
    "search",
    "ai",
    "model-context-protocol"
  ],
  "author": "kshern",
  "license": "MIT",
  "description": "A Model Context Protocol (MCP) server implementation for Tavily API, providing advanced search and content extraction capabilities",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/kshern/mcp-tavily.git"
  },
  "bugs": {
    "url": "https://github.com/kshern/mcp-tavily/issues"
  },
  "homepage": "https://github.com/kshern/mcp-tavily#readme",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.5.0",
    "@tavily/core": "^0.3.1",
    "@types/node": "^22.13.4",
    "typescript": "^5.7.3",
    "zod": "^3.24.2"
  }
}
```

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

```typescript
#!/usr/bin/env node

import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { tavily } from "@tavily/core";

// 初始化 Tavily 客户端
const tvly = tavily({ apiKey: process.env.TAVILY_API_KEY });

// 创建MCP服务器
const server = new McpServer({
  name: "Tavily Search MCP Server",
  version: "1.0.0"
});

// 添加搜索工具
server.tool(
  "search",
  "Perform a basic web search. Returns search results including title, content and URL.",
  {
    query: z.string().describe("Enter your search query or question"),
    options: z
      .object({
        searchDepth: z.enum(["basic", "advanced"]).optional().describe("Search depth: basic (simple search) or advanced (in-depth search)"),
        topic: z.enum(["general", "news", "finance"]).optional().describe("Search topic: general (all topics), news (news only), finance (financial content)"),
        days: z.number().optional().describe("Limit search to recent days, e.g.: 7 for last 7 days"),
        maxResults: z.number().optional().describe("Maximum number of results to return, e.g.: 10 for 10 results"),
        includeImages: z.boolean().optional().describe("Include images in results: true or false"),
        includeImageDescriptions: z.boolean().optional().describe("Include image descriptions: true or false"),
        includeAnswer: z.boolean().optional().describe("Include AI-generated answer summary: true or false"),
        includeRawContent: z.boolean().optional().describe("Include raw webpage content: true or false"),
        includeDomains: z.array(z.string()).optional().describe("Only search within these domains, e.g.: ['example.com', 'test.com']"),
        excludeDomains: z.array(z.string()).optional().describe("Exclude these domains from search, e.g.: ['example.com', 'test.com']"),
        maxTokens: z.number().optional().describe("Maximum number of tokens in response, e.g.: 1000"),
        timeRange: z.enum(["year", "month", "week", "day", "y", "m", "w", "d"]).optional().describe("Time range: year/y (within 1 year), month/m (within 1 month), week/w (within 1 week), day/d (within 1 day)")
      }).optional().describe("Search configuration options, all fields are optional")
  },
  async ({ query, options = {} }) => {
    try {
      const response = await tvly.search(query, options);
      const results = response.results;

      const content = results.map(result => ({
        type: "text" as const,
        text: `${result.title}\n${result.content}\nURL: ${result.url}\n\n`
      }));

      return {
        content: content
      };
    } catch (error:any) {
      throw new Error(`Search failed: ${error.message}`);
    }
  }
);

// 添加搜索工具
server.tool(
  "searchContext",
  "Perform a context-aware web search. Optimized for retrieving contextually relevant results.",
  {
    query: z.string().describe("Enter your search query or question"),
    options: z.object({
      searchDepth: z.enum(["basic", "advanced"]).optional().describe("Search depth: basic (simple search) or advanced (in-depth search)"),
      topic: z.enum(["general", "news", "finance"]).optional().describe("Search topic: general (all topics), news (news only), finance (financial content)"),
      days: z.number().optional().describe("Limit search to recent days, e.g.: 7 for last 7 days"),
      maxResults: z.number().optional().describe("Maximum number of results to return, e.g.: 10 for 10 results"),
      includeImages: z.boolean().optional().describe("Include images in results: true or false"),
      includeImageDescriptions: z.boolean().optional().describe("Include image descriptions: true or false"),
      includeAnswer: z.boolean().optional().describe("Include AI-generated answer summary: true or false"),
      includeRawContent: z.boolean().optional().describe("Include raw webpage content: true or false"),
      includeDomains: z.array(z.string()).optional().describe("Only search within these domains, e.g.: ['example.com', 'test.com']"),
      excludeDomains: z.array(z.string()).optional().describe("Exclude these domains from search, e.g.: ['example.com', 'test.com']"),
      maxTokens: z.number().optional().describe("Maximum number of tokens in response, e.g.: 1000"),
      timeRange: z.enum(["year", "month", "week", "day", "y", "m", "w", "d"]).optional().describe("Time range: year/y (within 1 year), month/m (within 1 month), week/w (within 1 week), day/d (within 1 day)")
    }).optional().describe("Search configuration options, all fields are optional")
  },
  async ({ query, options = {} }) => {
    try {
      const response = await tvly.search(query, options);
      const results = response.results;

      const content = results.map(result => ({
        type: "text" as const,
        text: `${result.title}\n${result.content}\nURL: ${result.url}\n\n`
      }));

      return {
        content: content
      };
    } catch (error:any) {
      throw new Error(`Search failed: ${error.message}`);
    }
  }
);

// 添加搜索工具
server.tool(
  "searchQNA",
  "Perform a question-answering search. Best suited for direct questions that need specific answers.",
  {
    query: z.string().describe("Enter your search query or question"),
    options: z.object({
      searchDepth: z.enum(["basic", "advanced"]).optional().describe("Search depth: basic (simple search) or advanced (in-depth search)"),
      topic: z.enum(["general", "news", "finance"]).optional().describe("Search topic: general (all topics), news (news only), finance (financial content)"),
      days: z.number().optional().describe("Limit search to recent days, e.g.: 7 for last 7 days"),
      maxResults: z.number().optional().describe("Maximum number of results to return, e.g.: 10 for 10 results"),
      includeImages: z.boolean().optional().describe("Include images in results: true or false"),
      includeImageDescriptions: z.boolean().optional().describe("Include image descriptions: true or false"),
      includeAnswer: z.boolean().optional().describe("Include AI-generated answer summary: true or false"),
      includeRawContent: z.boolean().optional().describe("Include raw webpage content: true or false"),
      includeDomains: z.array(z.string()).optional().describe("Only search within these domains, e.g.: ['example.com', 'test.com']"),
      excludeDomains: z.array(z.string()).optional().describe("Exclude these domains from search, e.g.: ['example.com', 'test.com']"),
      maxTokens: z.number().optional().describe("Maximum number of tokens in response, e.g.: 1000"),
      timeRange: z.enum(["year", "month", "week", "day", "y", "m", "w", "d"]).optional().describe("Time range: year/y (within 1 year), month/m (within 1 month), week/w (within 1 week), day/d (within 1 day)")
    }).optional().describe("Search configuration options, all fields are optional")
  },
  async ({ query, options = {} }) => {
    try {
      const response = await tvly.search(query, options);
      const results = response.results;

      const content = results.map(result => ({
        type: "text" as const,
        text: `${result.title}\n${result.content}\nURL: ${result.url}\n\n`
      }));

      return {
        content: content
      };
    } catch (error:any) {
      throw new Error(`Search failed: ${error.message}`);
    }
  }
);

// 添加extract工具
server.tool(
  "extract",
  "Extract and process content from a list of URLs. Can handle up to 20 URLs at once.",
  {
    urls: z.array(z.string()).describe("List of URLs to extract content from (max 20). e.g.: ['https://example.com', 'https://test.com']"),
    options: z.object({
      extractDepth: z.enum(["basic", "advanced"]).optional().describe("Extraction depth: basic (simple extraction) or advanced (detailed extraction)"),
      includeImages: z.boolean().optional().describe("Include images in extraction: true or false"),
    }).optional().describe("Content extraction configuration options, all fields are optional")
  },
  async ({ urls, options={} }) => {
    try {
      const response = await tvly.extract(urls, options);

      const content = response.results.map(result => ({
        type: "text" as const,
        text: `URL: ${result.url}\n内容: ${result.rawContent}\n\n`
      }));

      // 如果有失败的URL,也返回这些信息
      if (response.failedResults && response.failedResults.length > 0) {
        content.push({
          type: "text" as const,
          text: `\nFailed to extract from URLs:\n${response.failedResults.join('\n')}`
        });
      }

      return {
        content: content
      };
    } catch (error: any) {
      throw new Error(`Failed to extract content: ${error.message}`);
    }
  }
);

// 启动服务器,使用标准输入输出作为传输层
const transport = new StdioServerTransport();
await server.connect(transport);

```