#
tokens: 5699/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | 
3 | dist/
```

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

```markdown
  1 | # MCP Tavily
  2 | 
  3 | [![smithery badge](https://smithery.ai/badge/@kshern/mcp-tavily)](https://smithery.ai/server/@kshern/mcp-tavily)
  4 | 
  5 | [中文文档](./readme.zh-CN.md)
  6 | 
  7 | A Model Context Protocol (MCP) server implementation for Tavily API, providing advanced search and content extraction capabilities.
  8 | 
  9 | ## Features
 10 | 
 11 | - **Multiple Search Tools**:
 12 |   - `search`: Basic search functionality with customizable options
 13 |   - `searchContext`: Context-aware search for better relevance
 14 |   - `searchQNA`: Question and answer focused search
 15 | - **Content Extraction**: Extract content from URLs with configurable options
 16 | - **Rich Configuration Options**: Extensive options for search depth, filtering, and content inclusion
 17 | 
 18 | 
 19 | ### Usage with MCP
 20 | 
 21 | Add the Tavily MCP server to your MCP configuration:
 22 | 
 23 | ```json
 24 | {
 25 |   "mcpServers": {
 26 |     "tavily": {
 27 |       "command": "npx",
 28 |       "args": ["-y", "@mcptools/mcp-tavily"],
 29 |       "env": {
 30 |         "TAVILY_API_KEY": "your-api-key"
 31 |       }
 32 |     }
 33 |   }
 34 | }
 35 | ```
 36 | 
 37 | > 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.
 38 | 
 39 | ## API Reference
 40 | 
 41 | ### Search Tools
 42 | 
 43 | The server provides three search tools that can be called through MCP:
 44 | 
 45 | #### 1. Basic Search
 46 | ```typescript
 47 | // Tool name: search
 48 | {
 49 |   query: "artificial intelligence",
 50 |   options: {
 51 |     searchDepth: "advanced",
 52 |     topic: "news",
 53 |     maxResults: 10
 54 |   }
 55 | }
 56 | ```
 57 | 
 58 | #### 2. Context Search
 59 | ```typescript
 60 | // Tool name: searchContext
 61 | {
 62 |   query: "latest developments in AI",
 63 |   options: {
 64 |     topic: "news",
 65 |     timeRange: "week"
 66 |   }
 67 | }
 68 | ```
 69 | 
 70 | #### 3. Q&A Search
 71 | ```typescript
 72 | // Tool name: searchQNA
 73 | {
 74 |   query: "What is quantum computing?",
 75 |   options: {
 76 |     includeAnswer: true,
 77 |     maxResults: 5
 78 |   }
 79 | }
 80 | ```
 81 | 
 82 | ### Extract Tool
 83 | 
 84 | ```typescript
 85 | // Tool name: extract
 86 | {
 87 |   urls: ["https://example.com/article1", "https://example.com/article2"],
 88 |   options: {
 89 |     extractDepth: "advanced",
 90 |     includeImages: true
 91 |   }
 92 | }
 93 | ```
 94 | 
 95 | ### Search Options
 96 | 
 97 | All search tools share these options:
 98 | 
 99 | ```typescript
100 | interface SearchOptions {
101 |   searchDepth?: "basic" | "advanced";    // Search depth level
102 |   topic?: "general" | "news" | "finance"; // Search topic category
103 |   days?: number;                         // Number of days to search
104 |   maxResults?: number;                   // Maximum number of results
105 |   includeImages?: boolean;               // Include images in results
106 |   includeImageDescriptions?: boolean;    // Include image descriptions
107 |   includeAnswer?: boolean;               // Include answer in results
108 |   includeRawContent?: boolean;           // Include raw content
109 |   includeDomains?: string[];            // List of domains to include
110 |   excludeDomains?: string[];            // List of domains to exclude
111 |   maxTokens?: number;                    // Maximum number of tokens
112 |   timeRange?: "year" | "month" | "week" | "day" | "y" | "m" | "w" | "d"; // Time range for search
113 | }
114 | ```
115 | 
116 | ### Extract Options
117 | 
118 | ```typescript
119 | interface ExtractOptions {
120 |   extractDepth?: "basic" | "advanced";   // Extraction depth level
121 |   includeImages?: boolean;               // Include images in results
122 | }
123 | ```
124 | 
125 | ## Response Format
126 | 
127 | All tools return responses in the following format:
128 | 
129 | ```typescript
130 | {
131 |   content: Array<{
132 |     type: "text",
133 |     text: string
134 |   }>
135 | }
136 | ```
137 | 
138 | For search results, each item includes:
139 | - Title
140 | - Content
141 | - URL
142 | 
143 | For extracted content, each item includes:
144 | - URL
145 | - Raw content
146 | - Failed URLs list (if any)
147 | 
148 | ## Error Handling
149 | 
150 | All tools include proper error handling and will throw descriptive error messages if something goes wrong.
151 | 
152 | ## Installation
153 | 
154 | ### Installing via Smithery
155 | 
156 | To install Tavily API Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@kshern/mcp-tavily):
157 | 
158 | ```bash
159 | npx -y @smithery/cli install @kshern/mcp-tavily --client claude
160 | ```
161 | 
162 | ### Manual Installation
163 | ```bash
164 | npm install @mcptools/mcp-tavily
165 | ```
166 | 
167 | Or use it directly with npx:
168 | 
169 | ```bash
170 | npx @mcptools/mcp-tavily
171 | ```
172 | 
173 | 
174 | 
175 | ### Prerequisites
176 | 
177 | - Node.js 16 or higher
178 | - npm or yarn
179 | - Tavily API key (get one from [Tavily](https://tavily.com))
180 | 
181 | ### Setup
182 | 
183 | 1. Clone the repository
184 | 2. Install dependencies:
185 | ```bash
186 | npm install
187 | ```
188 | 3. Set your Tavily API key:
189 | ```bash
190 | export TAVILY_API_KEY=your_api_key
191 | ```
192 | 
193 | 
194 | ### Building
195 | 
196 | ```bash
197 | npm run build
198 | ```
199 | 
200 | ## Debugging with MCP Inspector
201 | 
202 | For development and debugging, we recommend using [MCP Inspector](https://github.com/modelcontextprotocol/inspector), a powerful development tool for MCP servers.
203 | 
204 | 
205 | The Inspector provides a user interface for:
206 | - Testing tool calls
207 | - Viewing server responses
208 | - Debugging tool execution
209 | - Monitoring server state
210 | 
211 | ## Contributing
212 | 
213 | Contributions are welcome! Please feel free to submit a Pull Request.
214 | 
215 | 1. Fork the repository
216 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
217 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
218 | 4. Push to the branch (`git push origin feature/AmazingFeature`)
219 | 5. Open a Pull Request
220 | 
221 | ## License
222 | 
223 | This project is licensed under the MIT License.
224 | 
225 | ## Support
226 | 
227 | For any questions or issues:
228 | - Tavily API: refer to the [Tavily documentation](https://docs.tavily.com/)
229 | - MCP integration: refer to the [MCP documentation](https://modelcontextprotocol.io//)
```

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

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "ES2022",
 5 |     "moduleResolution": "node",
 6 |     "esModuleInterop": true,
 7 |     "strict": true,
 8 |     "outDir": "dist",
 9 |     "rootDir": "src",
10 |     "declaration": true
11 |   },
12 |   "include": ["src/**/*"]
13 | }
14 | 
```

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

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:lts-alpine
 3 | 
 4 | # Create app directory
 5 | WORKDIR /app
 6 | 
 7 | # Install app dependencies
 8 | COPY package.json package-lock.json ./
 9 | RUN npm install --ignore-scripts
10 | 
11 | # Bundle app source code
12 | COPY . .
13 | 
14 | # Build the TypeScript code
15 | RUN npm run build
16 | 
17 | # Expose no ports - using stdio for communication
18 | 
19 | # Start the MCP server
20 | CMD [ "npm", "start" ]
21 | 
```

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

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

```json
 1 | {
 2 |   "name": "@mcptools/mcp-tavily",
 3 |   "version": "1.0.0",
 4 |   "type": "module",
 5 |   "main": "dist/index.js",
 6 |   "types": "dist/index.d.ts",
 7 |   "bin": {
 8 |     "mcp-tavily": "./dist/index.js"
 9 |   },
10 |   "files": [
11 |     "dist",
12 |     "README.md"
13 |   ],
14 |   "scripts": {
15 |     "build": "tsc",
16 |     "dev": "tsc --watch",
17 |     "prepublishOnly": "npm run build",
18 |     "test": "echo \"Error: no test specified\" && exit 1"
19 |   },
20 |   "keywords": [
21 |     "mcp",
22 |     "tavily",
23 |     "search",
24 |     "ai",
25 |     "model-context-protocol"
26 |   ],
27 |   "author": "kshern",
28 |   "license": "MIT",
29 |   "description": "A Model Context Protocol (MCP) server implementation for Tavily API, providing advanced search and content extraction capabilities",
30 |   "repository": {
31 |     "type": "git",
32 |     "url": "git+https://github.com/kshern/mcp-tavily.git"
33 |   },
34 |   "bugs": {
35 |     "url": "https://github.com/kshern/mcp-tavily/issues"
36 |   },
37 |   "homepage": "https://github.com/kshern/mcp-tavily#readme",
38 |   "dependencies": {
39 |     "@modelcontextprotocol/sdk": "^1.5.0",
40 |     "@tavily/core": "^0.3.1",
41 |     "@types/node": "^22.13.4",
42 |     "typescript": "^5.7.3",
43 |     "zod": "^3.24.2"
44 |   }
45 | }
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import { z } from "zod";
  6 | import { tavily } from "@tavily/core";
  7 | 
  8 | // 初始化 Tavily 客户端
  9 | const tvly = tavily({ apiKey: process.env.TAVILY_API_KEY });
 10 | 
 11 | // 创建MCP服务器
 12 | const server = new McpServer({
 13 |   name: "Tavily Search MCP Server",
 14 |   version: "1.0.0"
 15 | });
 16 | 
 17 | // 添加搜索工具
 18 | server.tool(
 19 |   "search",
 20 |   "Perform a basic web search. Returns search results including title, content and URL.",
 21 |   {
 22 |     query: z.string().describe("Enter your search query or question"),
 23 |     options: z
 24 |       .object({
 25 |         searchDepth: z.enum(["basic", "advanced"]).optional().describe("Search depth: basic (simple search) or advanced (in-depth search)"),
 26 |         topic: z.enum(["general", "news", "finance"]).optional().describe("Search topic: general (all topics), news (news only), finance (financial content)"),
 27 |         days: z.number().optional().describe("Limit search to recent days, e.g.: 7 for last 7 days"),
 28 |         maxResults: z.number().optional().describe("Maximum number of results to return, e.g.: 10 for 10 results"),
 29 |         includeImages: z.boolean().optional().describe("Include images in results: true or false"),
 30 |         includeImageDescriptions: z.boolean().optional().describe("Include image descriptions: true or false"),
 31 |         includeAnswer: z.boolean().optional().describe("Include AI-generated answer summary: true or false"),
 32 |         includeRawContent: z.boolean().optional().describe("Include raw webpage content: true or false"),
 33 |         includeDomains: z.array(z.string()).optional().describe("Only search within these domains, e.g.: ['example.com', 'test.com']"),
 34 |         excludeDomains: z.array(z.string()).optional().describe("Exclude these domains from search, e.g.: ['example.com', 'test.com']"),
 35 |         maxTokens: z.number().optional().describe("Maximum number of tokens in response, e.g.: 1000"),
 36 |         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)")
 37 |       }).optional().describe("Search configuration options, all fields are optional")
 38 |   },
 39 |   async ({ query, options = {} }) => {
 40 |     try {
 41 |       const response = await tvly.search(query, options);
 42 |       const results = response.results;
 43 | 
 44 |       const content = results.map(result => ({
 45 |         type: "text" as const,
 46 |         text: `${result.title}\n${result.content}\nURL: ${result.url}\n\n`
 47 |       }));
 48 | 
 49 |       return {
 50 |         content: content
 51 |       };
 52 |     } catch (error:any) {
 53 |       throw new Error(`Search failed: ${error.message}`);
 54 |     }
 55 |   }
 56 | );
 57 | 
 58 | // 添加搜索工具
 59 | server.tool(
 60 |   "searchContext",
 61 |   "Perform a context-aware web search. Optimized for retrieving contextually relevant results.",
 62 |   {
 63 |     query: z.string().describe("Enter your search query or question"),
 64 |     options: z.object({
 65 |       searchDepth: z.enum(["basic", "advanced"]).optional().describe("Search depth: basic (simple search) or advanced (in-depth search)"),
 66 |       topic: z.enum(["general", "news", "finance"]).optional().describe("Search topic: general (all topics), news (news only), finance (financial content)"),
 67 |       days: z.number().optional().describe("Limit search to recent days, e.g.: 7 for last 7 days"),
 68 |       maxResults: z.number().optional().describe("Maximum number of results to return, e.g.: 10 for 10 results"),
 69 |       includeImages: z.boolean().optional().describe("Include images in results: true or false"),
 70 |       includeImageDescriptions: z.boolean().optional().describe("Include image descriptions: true or false"),
 71 |       includeAnswer: z.boolean().optional().describe("Include AI-generated answer summary: true or false"),
 72 |       includeRawContent: z.boolean().optional().describe("Include raw webpage content: true or false"),
 73 |       includeDomains: z.array(z.string()).optional().describe("Only search within these domains, e.g.: ['example.com', 'test.com']"),
 74 |       excludeDomains: z.array(z.string()).optional().describe("Exclude these domains from search, e.g.: ['example.com', 'test.com']"),
 75 |       maxTokens: z.number().optional().describe("Maximum number of tokens in response, e.g.: 1000"),
 76 |       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)")
 77 |     }).optional().describe("Search configuration options, all fields are optional")
 78 |   },
 79 |   async ({ query, options = {} }) => {
 80 |     try {
 81 |       const response = await tvly.search(query, options);
 82 |       const results = response.results;
 83 | 
 84 |       const content = results.map(result => ({
 85 |         type: "text" as const,
 86 |         text: `${result.title}\n${result.content}\nURL: ${result.url}\n\n`
 87 |       }));
 88 | 
 89 |       return {
 90 |         content: content
 91 |       };
 92 |     } catch (error:any) {
 93 |       throw new Error(`Search failed: ${error.message}`);
 94 |     }
 95 |   }
 96 | );
 97 | 
 98 | // 添加搜索工具
 99 | server.tool(
100 |   "searchQNA",
101 |   "Perform a question-answering search. Best suited for direct questions that need specific answers.",
102 |   {
103 |     query: z.string().describe("Enter your search query or question"),
104 |     options: z.object({
105 |       searchDepth: z.enum(["basic", "advanced"]).optional().describe("Search depth: basic (simple search) or advanced (in-depth search)"),
106 |       topic: z.enum(["general", "news", "finance"]).optional().describe("Search topic: general (all topics), news (news only), finance (financial content)"),
107 |       days: z.number().optional().describe("Limit search to recent days, e.g.: 7 for last 7 days"),
108 |       maxResults: z.number().optional().describe("Maximum number of results to return, e.g.: 10 for 10 results"),
109 |       includeImages: z.boolean().optional().describe("Include images in results: true or false"),
110 |       includeImageDescriptions: z.boolean().optional().describe("Include image descriptions: true or false"),
111 |       includeAnswer: z.boolean().optional().describe("Include AI-generated answer summary: true or false"),
112 |       includeRawContent: z.boolean().optional().describe("Include raw webpage content: true or false"),
113 |       includeDomains: z.array(z.string()).optional().describe("Only search within these domains, e.g.: ['example.com', 'test.com']"),
114 |       excludeDomains: z.array(z.string()).optional().describe("Exclude these domains from search, e.g.: ['example.com', 'test.com']"),
115 |       maxTokens: z.number().optional().describe("Maximum number of tokens in response, e.g.: 1000"),
116 |       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)")
117 |     }).optional().describe("Search configuration options, all fields are optional")
118 |   },
119 |   async ({ query, options = {} }) => {
120 |     try {
121 |       const response = await tvly.search(query, options);
122 |       const results = response.results;
123 | 
124 |       const content = results.map(result => ({
125 |         type: "text" as const,
126 |         text: `${result.title}\n${result.content}\nURL: ${result.url}\n\n`
127 |       }));
128 | 
129 |       return {
130 |         content: content
131 |       };
132 |     } catch (error:any) {
133 |       throw new Error(`Search failed: ${error.message}`);
134 |     }
135 |   }
136 | );
137 | 
138 | // 添加extract工具
139 | server.tool(
140 |   "extract",
141 |   "Extract and process content from a list of URLs. Can handle up to 20 URLs at once.",
142 |   {
143 |     urls: z.array(z.string()).describe("List of URLs to extract content from (max 20). e.g.: ['https://example.com', 'https://test.com']"),
144 |     options: z.object({
145 |       extractDepth: z.enum(["basic", "advanced"]).optional().describe("Extraction depth: basic (simple extraction) or advanced (detailed extraction)"),
146 |       includeImages: z.boolean().optional().describe("Include images in extraction: true or false"),
147 |     }).optional().describe("Content extraction configuration options, all fields are optional")
148 |   },
149 |   async ({ urls, options={} }) => {
150 |     try {
151 |       const response = await tvly.extract(urls, options);
152 | 
153 |       const content = response.results.map(result => ({
154 |         type: "text" as const,
155 |         text: `URL: ${result.url}\n内容: ${result.rawContent}\n\n`
156 |       }));
157 | 
158 |       // 如果有失败的URL,也返回这些信息
159 |       if (response.failedResults && response.failedResults.length > 0) {
160 |         content.push({
161 |           type: "text" as const,
162 |           text: `\nFailed to extract from URLs:\n${response.failedResults.join('\n')}`
163 |         });
164 |       }
165 | 
166 |       return {
167 |         content: content
168 |       };
169 |     } catch (error: any) {
170 |       throw new Error(`Failed to extract content: ${error.message}`);
171 |     }
172 |   }
173 | );
174 | 
175 | // 启动服务器,使用标准输入输出作为传输层
176 | const transport = new StdioServerTransport();
177 | await server.connect(transport);
178 | 
```