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

```
1 | 
```

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

```
 1 | # Dependencies
 2 | node_modules/
 3 | npm-debug.log*
 4 | yarn-debug.log*
 5 | yarn-error.log*
 6 | 
 7 | # Build output
 8 | build/
 9 | dist/
10 | *.tsbuildinfo
11 | 
12 | # Environment variables
13 | .env
14 | .env.local
15 | .env.*.local
16 | 
17 | # IDE and editor files
18 | .idea/
19 | .vscode/
20 | *.swp
21 | *.swo
22 | .DS_Store
23 | 
24 | # Logs
25 | logs/
26 | *.log
27 | 
28 | # Testing
29 | coverage/
30 | 
31 | # Misc
32 | .tmp/
33 | .temp/
34 | 
35 | # Data storage
36 | data/*.json
37 | # Keep the data directory but ignore its contents
38 | !data/.gitkeep
39 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Tavily MCP Server
  2 | 
  3 | 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.
  4 | 
  5 | ## Features
  6 | 
  7 | - AI-powered search functionality
  8 | - Support for basic and advanced search depths
  9 | - Rich search results including titles, URLs, and content snippets
 10 | - AI-generated summaries of search results
 11 | - Result scoring and response time tracking
 12 | - Comprehensive search history storage with caching
 13 | - MCP Resources for flexible data access
 14 | 
 15 | ## Prerequisites
 16 | 
 17 | - Node.js (v16 or higher)
 18 | - npm (Node Package Manager)
 19 | - Tavily API key (Get one at [Tavily's website](https://tavily.com))
 20 | - An MCP client (e.g., Cline, Claude Desktop, or your own implementation)
 21 | 
 22 | ## Installation
 23 | 
 24 | 1. Clone the repository:
 25 | ```bash
 26 | git clone https://github.com/it-beard/tavily-server.git
 27 | cd tavily-mcp-server
 28 | ```
 29 | 
 30 | 2. Install dependencies:
 31 | ```bash
 32 | npm install
 33 | ```
 34 | 
 35 | 3. Build the project:
 36 | ```bash
 37 | npm run build
 38 | ```
 39 | 
 40 | ## Configuration
 41 | 
 42 | This server can be used with any MCP client. Below are configuration instructions for popular clients:
 43 | 
 44 | ### Cline Configuration
 45 | 
 46 | If you're using Cline (the VSCode extension for Claude), create or modify the MCP settings file at:
 47 | - macOS: `~/Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
 48 | - Windows: `%APPDATA%\Cursor\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json`
 49 | - Linux: `~/.config/Cursor/User/globalStorage/saoudrizwan.claude-dev\settings\cline_mcp_settings.json`
 50 | 
 51 | Add the following configuration (replace paths and API key with your own):
 52 | ```json
 53 | {
 54 |   "mcpServers": {
 55 |     "tavily": {
 56 |       "command": "node",
 57 |       "args": ["/path/to/tavily-server/build/index.js"],
 58 |       "env": {
 59 |         "TAVILY_API_KEY": "your-api-key-here"
 60 |       }
 61 |     }
 62 |   }
 63 | }
 64 | ```
 65 | 
 66 | ### Claude Desktop Configuration
 67 | 
 68 | If you're using the Claude Desktop app, modify the configuration file at:
 69 | - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
 70 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
 71 | - Linux: `~/.config/Claude/claude_desktop_config.json`
 72 | 
 73 | Use the same configuration format as shown above.
 74 | 
 75 | ### Other MCP Clients
 76 | 
 77 | For other MCP clients, consult their documentation for the correct configuration file location and format. The server configuration should include:
 78 | 1. Command to run the server (typically `node`)
 79 | 2. Path to the compiled server file
 80 | 3. Environment variables including the Tavily API key
 81 | 
 82 | ## Usage
 83 | 
 84 | ### Tools
 85 | 
 86 | The server provides a single tool named `search` with the following parameters:
 87 | 
 88 | #### Required Parameters
 89 | - `query` (string): The search query to execute
 90 | 
 91 | #### Optional Parameters
 92 | - `search_depth` (string): Either "basic" (faster) or "advanced" (more comprehensive)
 93 | 
 94 | #### Example Usage
 95 | 
 96 | ```typescript
 97 | // Example using the MCP SDK
 98 | const result = await mcpClient.callTool("tavily", "search", {
 99 |   query: "latest developments in artificial intelligence",
100 |   search_depth: "basic"
101 | });
102 | ```
103 | 
104 | ### Resources
105 | 
106 | The server provides both static and dynamic resources for flexible data access:
107 | 
108 | #### Static Resources
109 | - `tavily://last-search/result`: Returns the results of the most recent search query
110 |   - Persisted to disk in the data directory
111 |   - Survives server restarts
112 |   - Returns a 'No search has been performed yet' error if no search has been done
113 | 
114 | #### Dynamic Resources (Resource Templates)
115 | - `tavily://search/{query}`: Access search results for any query
116 |   - Replace {query} with your URL-encoded search term
117 |   - Example: `tavily://search/artificial%20intelligence`
118 |   - Returns cached results if the query was previously made
119 |   - Performs and stores new search if query hasn't been searched before
120 |   - Returns the same format as the search tool but through a resource interface
121 | 
122 | Resources in MCP provide an alternative way to access data compared to tools:
123 | - Tools are for executing operations (like performing a new search)
124 | - Resources are for accessing data (like retrieving existing search results)
125 | - Resource URIs can be stored and accessed later
126 | - Resources support both static (fixed) and dynamic (templated) access patterns
127 | 
128 | #### Response Format
129 | 
130 | ```typescript
131 | interface SearchResponse {
132 |   query: string;
133 |   answer: string;
134 |   results: Array<{
135 |     title: string;
136 |     url: string;
137 |     content: string;
138 |     score: number;
139 |   }>;
140 |   response_time: number;
141 | }
142 | ```
143 | 
144 | ### Persistent Storage
145 | 
146 | The server implements comprehensive persistent storage for search results:
147 | 
148 | #### Storage Location
149 | - Data is stored in the `data` directory
150 | - `data/searches.json` contains all historical search results
151 | - Data persists between server restarts
152 | - Storage is automatically initialized on server start
153 | 
154 | #### Storage Features
155 | - Stores complete search history
156 | - Caches all search results for quick retrieval
157 | - Automatic saving of new search results
158 | - Disk-based persistence
159 | - JSON format for easy debugging
160 | - Error handling for storage operations
161 | - Automatic directory creation
162 | 
163 | #### Caching Behavior
164 | - All search results are cached automatically
165 | - Subsequent requests for the same query return cached results
166 | - Caching improves response time and reduces API calls
167 | - Cache persists between server restarts
168 | - Last search is tracked for quick access
169 | 
170 | ## Development
171 | 
172 | ### Project Structure
173 | 
174 | ```
175 | tavily-server/
176 | ├── src/
177 | │   └── index.ts    # Main server implementation
178 | ├── data/           # Persistent storage directory
179 | │   └── searches.json  # Search history and cache storage
180 | ├── build/          # Compiled JavaScript files
181 | ├── package.json    # Project dependencies and scripts
182 | └── tsconfig.json   # TypeScript configuration
183 | ```
184 | 
185 | ### Available Scripts
186 | 
187 | - `npm run build`: Compile TypeScript and make the output executable
188 | - `npm run start`: Start the MCP server (after building)
189 | - `npm run dev`: Run the server in development mode
190 | 
191 | ## Error Handling
192 | 
193 | The server provides detailed error messages for common issues:
194 | - Invalid API key
195 | - Network errors
196 | - Invalid search parameters
197 | - API rate limiting
198 | - Resource not found
199 | - Invalid resource URIs
200 | - Storage read/write errors
201 | 
202 | ## Contributing
203 | 
204 | 1. Fork the repository
205 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
206 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
207 | 4. Push to the branch (`git push origin feature/amazing-feature`)
208 | 5. Open a Pull Request
209 | 
210 | ## License
211 | 
212 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
213 | 
214 | ## Acknowledgments
215 | 
216 | - [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol/protocol) for the server framework
217 | - [Tavily API](https://tavily.com) for providing the search capabilities
218 | 
```

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

```json
 1 | {
 2 |   "type": "module",
 3 |   "scripts": {
 4 |     "build": "tsc && chmod +x build/index.js"
 5 |   },
 6 |   "devDependencies": {
 7 |     "@types/node": "^22.10.2",
 8 |     "typescript": "^5.3.3"
 9 |   },
10 |   "dependencies": {
11 |     "@modelcontextprotocol/sdk": "^1.0.3",
12 |     "axios": "^1.7.9"
13 |   }
14 | }
15 | 
```

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

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

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

```typescript
  1 | #!/usr/bin/env node
  2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  4 | import {
  5 |   CallToolRequestSchema,
  6 |   ErrorCode,
  7 |   ListToolsRequestSchema,
  8 |   McpError,
  9 |   ListResourcesRequestSchema,
 10 |   ListResourceTemplatesRequestSchema,
 11 |   ReadResourceRequestSchema,
 12 | } from '@modelcontextprotocol/sdk/types.js';
 13 | import axios from 'axios';
 14 | import { mkdir, writeFile, readFile } from 'fs/promises';
 15 | import { join, dirname } from 'path';
 16 | import { fileURLToPath } from 'url';
 17 | 
 18 | const API_KEY = process.env.TAVILY_API_KEY;
 19 | if (!API_KEY) {
 20 |   throw new Error('TAVILY_API_KEY environment variable is required');
 21 | }
 22 | 
 23 | // Get the directory where the script is located
 24 | const __filename = fileURLToPath(import.meta.url);
 25 | const __dirname = dirname(__filename);
 26 | 
 27 | interface TavilySearchResponse {
 28 |   results: Array<{
 29 |     title: string;
 30 |     url: string;
 31 |     content: string;
 32 |   }>;
 33 |   query: string;
 34 | }
 35 | 
 36 | interface TavilyErrorResponse {
 37 |   message: string;
 38 |   status?: number;
 39 |   error?: string;
 40 | }
 41 | 
 42 | interface StoredSearches {
 43 |   searches: { [query: string]: TavilySearchResponse };
 44 |   lastQuery: string | null;
 45 | }
 46 | 
 47 | const isValidSearchArgs = (
 48 |   args: any
 49 | ): args is { query: string; search_depth?: 'basic' | 'advanced' } =>
 50 |   typeof args === 'object' &&
 51 |   args !== null &&
 52 |   typeof args.query === 'string' &&
 53 |   (args.search_depth === undefined ||
 54 |     args.search_depth === 'basic' ||
 55 |     args.search_depth === 'advanced');
 56 | 
 57 | class TavilyServer {
 58 |   private server: Server;
 59 |   private axiosInstance;
 60 |   private searches: StoredSearches = { searches: {}, lastQuery: null };
 61 |   private dataDir: string;
 62 |   private storageFile: string;
 63 | 
 64 |   constructor() {
 65 |     this.server = new Server(
 66 |       {
 67 |         name: 'tavily-search-server',
 68 |         version: '0.1.0',
 69 |       },
 70 |       {
 71 |         capabilities: {
 72 |           tools: {},
 73 |           resources: {},
 74 |         },
 75 |       }
 76 |     );
 77 | 
 78 |     this.axiosInstance = axios.create({
 79 |       baseURL: 'https://api.tavily.com',
 80 |       headers: {
 81 |         'Content-Type': 'application/json',
 82 |         'api-key': API_KEY,
 83 |       },
 84 |     });
 85 | 
 86 |     // Set up data storage paths
 87 |     this.dataDir = join(__dirname, '..', 'data');
 88 |     this.storageFile = join(this.dataDir, 'searches.json');
 89 | 
 90 |     this.setupToolHandlers();
 91 |     this.setupResourceHandlers();
 92 |     
 93 |     // Error handling
 94 |     this.server.onerror = (error) => console.error('[MCP Error]', error);
 95 |     process.on('SIGINT', async () => {
 96 |       await this.server.close();
 97 |       process.exit(0);
 98 |     });
 99 |   }
100 | 
101 |   private async initializeStorage() {
102 |     try {
103 |       // Create data directory if it doesn't exist
104 |       await mkdir(this.dataDir, { recursive: true });
105 |       
106 |       // Try to load existing data
107 |       try {
108 |         const data = await readFile(this.storageFile, 'utf-8');
109 |         this.searches = JSON.parse(data);
110 |       } catch (error) {
111 |         // File doesn't exist or is invalid, initialize with empty state
112 |         this.searches = { searches: {}, lastQuery: null };
113 |         await this.saveSearches();
114 |       }
115 |     } catch (error) {
116 |       console.error('Failed to initialize storage:', error);
117 |       throw new Error('Failed to initialize storage');
118 |     }
119 |   }
120 | 
121 |   private async saveSearches() {
122 |     try {
123 |       await writeFile(this.storageFile, JSON.stringify(this.searches, null, 2), 'utf-8');
124 |     } catch (error) {
125 |       console.error('Failed to save searches:', error);
126 |       throw new Error('Failed to save searches');
127 |     }
128 |   }
129 | 
130 |   private async saveSearch(query: string, result: TavilySearchResponse) {
131 |     this.searches.searches[query] = result;
132 |     this.searches.lastQuery = query;
133 |     await this.saveSearches();
134 |   }
135 | 
136 |   private setupResourceHandlers() {
137 |     // List available static resources
138 |     this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
139 |       resources: [
140 |         {
141 |           uri: 'tavily://last-search/result',
142 |           name: 'Last Search Result',
143 |           description: 'Results from the most recent search query',
144 |           mimeType: 'application/json',
145 |         }
146 |       ],
147 |     }));
148 | 
149 |     // List resource templates for dynamic resources
150 |     this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
151 |       resourceTemplates: [
152 |         {
153 |           uriTemplate: 'tavily://search/{query}',
154 |           name: 'Search Results by Query',
155 |           description: 'Search results for a specific query',
156 |           mimeType: 'application/json',
157 |         },
158 |       ],
159 |     }));
160 | 
161 |     // Handle resource reading
162 |     this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
163 |       // Handle static resource: last search result
164 |       if (request.params.uri === 'tavily://last-search/result') {
165 |         if (!this.searches.lastQuery || !this.searches.searches[this.searches.lastQuery]) {
166 |           throw new McpError(
167 |             ErrorCode.InvalidRequest,
168 |             'No search has been performed yet'
169 |           );
170 |         }
171 |         return {
172 |           contents: [
173 |             {
174 |               uri: request.params.uri,
175 |               mimeType: 'application/json',
176 |               text: JSON.stringify(this.searches.searches[this.searches.lastQuery], null, 2),
177 |             },
178 |           ],
179 |         };
180 |       }
181 | 
182 |       // Handle dynamic resource: search by query
183 |       const searchMatch = request.params.uri.match(/^tavily:\/\/search\/(.+)$/);
184 |       if (searchMatch) {
185 |         const query = decodeURIComponent(searchMatch[1]);
186 |         
187 |         // First check if we already have this search stored
188 |         if (this.searches.searches[query]) {
189 |           return {
190 |             contents: [
191 |               {
192 |                 uri: request.params.uri,
193 |                 mimeType: 'application/json',
194 |                 text: JSON.stringify(this.searches.searches[query], null, 2),
195 |               },
196 |             ],
197 |           };
198 |         }
199 | 
200 |         // If not found in storage, perform new search
201 |         try {
202 |           const response = await this.axiosInstance.post<TavilySearchResponse>(
203 |             '/search',
204 |             {
205 |               api_key: API_KEY,
206 |               query,
207 |               search_depth: 'basic',
208 |               include_answer: true,
209 |               include_raw_content: false
210 |             }
211 |           );
212 | 
213 |           // Save the result
214 |           await this.saveSearch(query, response.data);
215 | 
216 |           return {
217 |             contents: [
218 |               {
219 |                 uri: request.params.uri,
220 |                 mimeType: 'application/json',
221 |                 text: JSON.stringify(response.data, null, 2),
222 |               },
223 |             ],
224 |           };
225 |         } catch (error) {
226 |           const axiosError = error as { response?: { data?: TavilyErrorResponse; status?: number }; message?: string };
227 |           throw new McpError(
228 |             ErrorCode.InternalError,
229 |             `Search failed: ${axiosError.response?.data?.message ?? axiosError.message}`
230 |           );
231 |         }
232 |       }
233 | 
234 |       throw new McpError(
235 |         ErrorCode.InvalidRequest,
236 |         `Invalid resource URI: ${request.params.uri}`
237 |       );
238 |     });
239 |   }
240 | 
241 |   private setupToolHandlers() {
242 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
243 |       tools: [
244 |         {
245 |           name: 'search',
246 |           description: 'Perform an AI-powered search using Tavily API',
247 |           inputSchema: {
248 |             type: 'object',
249 |             properties: {
250 |               query: {
251 |                 type: 'string',
252 |                 description: 'Search query',
253 |               },
254 |               search_depth: {
255 |                 type: 'string',
256 |                 enum: ['basic', 'advanced'],
257 |                 description: 'Search depth - basic is faster, advanced is more comprehensive',
258 |               },
259 |             },
260 |             required: ['query'],
261 |           },
262 |         },
263 |       ],
264 |     }));
265 | 
266 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
267 |       if (request.params.name !== 'search') {
268 |         throw new McpError(
269 |           ErrorCode.MethodNotFound,
270 |           `Unknown tool: ${request.params.name}`
271 |         );
272 |       }
273 | 
274 |       if (!isValidSearchArgs(request.params.arguments)) {
275 |         throw new McpError(
276 |           ErrorCode.InvalidParams,
277 |           'Invalid search arguments'
278 |         );
279 |       }
280 | 
281 |       try {
282 |         console.error('Making request to Tavily API...'); // Debug log
283 |         const response = await this.axiosInstance.post<TavilySearchResponse>(
284 |           '/search',
285 |           {
286 |             api_key: API_KEY,
287 |             query: request.params.arguments.query,
288 |             search_depth: request.params.arguments.search_depth || 'basic',
289 |             include_answer: true,
290 |             include_raw_content: false
291 |           }
292 |         );
293 | 
294 |         // Save the result
295 |         await this.saveSearch(request.params.arguments.query, response.data);
296 | 
297 |         console.error('Received response from Tavily API'); // Debug log
298 |         return {
299 |           content: [
300 |             {
301 |               type: 'text',
302 |               text: JSON.stringify(response.data, null, 2),
303 |             },
304 |           ],
305 |         };
306 |       } catch (error) {
307 |         console.error('Tavily API Error:', error); // Debug log
308 |         const axiosError = error as { response?: { data?: TavilyErrorResponse; status?: number }; message?: string };
309 |         const errorMessage = axiosError.response?.data?.message ?? 
310 |                            axiosError.response?.data?.error ??
311 |                            axiosError.message ??
312 |                            'Unknown error occurred';
313 |         const statusCode = axiosError.response?.status ?? 'unknown';
314 |         
315 |         console.error(`Error details - Message: ${errorMessage}, Status: ${statusCode}`); // Debug log
316 |         
317 |         return {
318 |           content: [
319 |             {
320 |               type: 'text',
321 |               text: `Tavily API error: ${errorMessage} (Status: ${statusCode})`,
322 |             },
323 |           ],
324 |           isError: true,
325 |         };
326 |       }
327 |     });
328 |   }
329 | 
330 |   async run() {
331 |     // Initialize storage before starting the server
332 |     await this.initializeStorage();
333 |     
334 |     const transport = new StdioServerTransport();
335 |     await this.server.connect(transport);
336 |     console.error('Tavily MCP server running on stdio');
337 |   }
338 | }
339 | 
340 | const server = new TavilyServer();
341 | server.run().catch(console.error);
342 | 
```