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