# 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 | [](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 | ```