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