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