From 14451043edccda987dbb6ff39d6b0876d44328db Mon Sep 17 00:00:00 2001 From: pulvedu Date: Mon, 27 Jan 2025 12:38:21 -0500 Subject: [PATCH] Inital MCP code commit --- .gitignore | 3 + README.md | 160 ++++++++++++++++++++++ package-lock.json | 301 ++++++++++++++++++++++++++++++++++++++++ package.json | 29 ++++ src/index.ts | 342 ++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 15 ++ 6 files changed, 850 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..864c692 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +build/ +.env* diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4773e3 --- /dev/null +++ b/README.md @@ -0,0 +1,160 @@ +# Tavily MCP Server 🚀 + +INCLUDE A EMBEDED VIDEO DEMO + +The Model Context Protocol (MCP) is an open standard that enables AI systems to interact seamlessly with various data sources and tools, facilitating secure, two-way connections. + +Developed by Anthropic, the Model Context Protocol (MCP) enables AI assistants like Claude to seamlessly integrate with Tavily's advanced search and data extraction capabilities. This integration provides AI models with real-time access to web information, complete with sophisticated filtering options and domain-specific search features. + +The Tavily MCP server provides: +- Seamless interation with the tavily-search and tavily-extract tools +- Real-time web search capabilities through the tavily-search tool +- Intelligent data extraction from web pages via the tavily-extract tool + + +## Usage 🔧 + +Before you begin, ensure you have: + +- [Tavily API key](https://tavily.com/api-keys) +- [Claude Desktop](https://claude.ai/download) +- [Node.js](https://nodejs.org/) (v20 or higher) + - You can verify your Node.js installation by running: + - `node --version` +- [Git](https://git-scm.com/downloads) installed + - On macOS: `brew install git` + - On Linux: + - Debian/Ubuntu: `sudo apt install git` + - RedHat/CentOS: `sudo yum install git` + - On Windows: Download [Git for Windows](https://git-scm.com/download/win) + +### Tavily MCP server installation 🔨 + +### NPM Installation + +```bash +npm install -g tavily-mcp +``` + +### Git Installation + +1. Clone the repository: +```bash +git clone https://github.com/tavily/tavily-mcp.git +cd tavily-mcp +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Build the project: +```bash +npm run build +``` + +## Configuration ⚙️ + +### 1. Configure Claude Desktop + +Enable Developer Mode in Claude Desktop and open the configuration file: + +#### For macOS: +```bash +code ~/Library/Application\ Support/Claude/claude_desktop_config.json +``` + +#### For Windows: +```bash +code %APPDATA%\Claude\claude_desktop_config.json +``` + +### 2. Add the Tavily server configuration: + +```json +{ + "mcpServers": { + "tavily": { + "command": "npx", + "args": ["/path/to/tavily-mcp/build/index.js"], + "env": { + "TAVILY_API_KEY": "your-api-key-here" + } + } + } +} +``` + +Replace `your-api-key-here` with your actual Tavily API key. + +## Usage 🎯 + +The server provides three main search tools: + +1. **General Web Search**: +``` +Can you search for recent developments in quantum computing? +``` + +2. **News Search**: +``` +Search for news articles about AI startups from the last 7 days. +``` + +3. **Domain-Specific Search**: +``` +Search for climate change research on nature.com and sciencedirect.com +``` + +## Features ✨ + +- **Multiple Search Types**: General web search, news search, and answer generation +- **Domain Filtering**: Include or exclude specific websites from search results +- **Time-Based Filtering**: Filter news by recency +- **Error Handling**: Graceful handling of API errors and rate limits +- **Type Safety**: Full TypeScript implementation with proper type checking + +## Development + +Install dependencies: +```bash +npm install +``` + +Build the server: +```bash +npm run build +``` + +For development with auto-rebuild: +```bash +npm run watch +``` + +## Troubleshooting 🔧 + +### Common Issues + +1. **Server Not Found** + - Verify the npm installation + - Check Claude Desktop configuration syntax + - Ensure Node.js is properly installed + +2. **API Key Issues** + - Confirm your Tavily API key is valid + - Check the API key is correctly set in the config + - Verify no spaces or quotes around the API key + +3. **Debugging** + - Use the MCP Inspector for detailed debugging: + ```bash + npm run inspector + ``` + +## Acknowledgments 🙏 + +- [Tavily](https://tavily.com) for their powerful search API +- [Model Context Protocol](https://modelcontextprotocol.io) for the MCP specification +- [Anthropic](https://anthropic.com) for Claude Desktop + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c415cf1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,301 @@ +{ + "name": "tavily-mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tavily-mcp", + "version": "0.1.0", + "dependencies": { + "@modelcontextprotocol/sdk": "0.6.0", + "axios": "^1.6.7", + "dotenv": "^16.4.5" + }, + "bin": { + "tavily-mcp": "build/index.js" + }, + "devDependencies": { + "@types/node": "^20.11.24", + "typescript": "^5.3.3" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.0.tgz", + "integrity": "sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@types/node": { + "version": "20.17.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.16.tgz", + "integrity": "sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c549f0c --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "tavily-mcp", + "version": "0.1.0", + "description": "MCP server for advanced web search using Tavily", + "private": true, + "type": "module", + "bin": { + "tavily-mcp": "./build/index.js" + }, + "files": [ + "build" + ], + "scripts": { + "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", + "prepare": "npm run build", + "watch": "tsc --watch", + "inspector": "npx @modelcontextprotocol/inspector build/index.js", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "0.6.0", + "dotenv": "^16.4.5", + "axios": "^1.6.7" + }, + "devDependencies": { + "@types/node": "^20.11.24", + "typescript": "^5.3.3" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..625f088 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,342 @@ +#!/usr/bin/env node + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import {CallToolRequestSchema, ListToolsRequestSchema, Tool} from "@modelcontextprotocol/sdk/types.js"; +import axios from "axios"; +import dotenv from "dotenv"; +import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; + +dotenv.config(); + +const API_KEY = process.env.TAVILY_API_KEY; +if (!API_KEY) { + throw new Error("TAVILY_API_KEY environment variable is required"); +} + + +interface TavilyResponse { + // Response structure from Tavily API + query: string; + follow_up_questions?: Array; + answer?: string; + images?: Array; + results: Array<{ + title: string; + url: string; + content: string; + score: number; + published_date?: string; + raw_content?: string; + }>; +} + +class TavilyClient { + // Core client properties + private server: Server; + private axiosInstance; + private baseURLs = { + search: 'https://api.tavily.com/search', + extract: 'https://api.tavily.com/extract' + }; + + constructor() { + this.server = new Server( + { + name: "tavily-mcp", + version: "0.1.0", + }, + { + capabilities: { + resources: {}, + tools: {}, + prompts: {}, + }, + } + ); + + this.axiosInstance = axios.create({ + headers: { + 'accept': 'application/json', + 'content-type': 'application/json', + 'x-api-key': API_KEY + } + }); + + this.setupHandlers(); + this.setupErrorHandling(); + } + + private setupErrorHandling(): void { + this.server.onerror = (error) => { + console.error("[MCP Error]", error); + }; + + process.on('SIGINT', async () => { + await this.server.close(); + process.exit(0); + }); + } + + private setupHandlers(): void { + this.setupToolHandlers(); + } + + private setupToolHandlers(): void { + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + // Define available tools: tavily-search and tavily-extract + const tools: Tool[] = [ + { + name: "tavily-search", + description: "A powerful web search tool that provides comprehensive, real-time results using Tavily's AI search engine. Returns relevant web content with customizable parameters for result count, content type, and domain filtering. Ideal for gathering current information, news, and detailed web content analysis.", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "Search query" + }, + search_depth: { + type: "string", + enum: ["basic","advanced"], + description: "The depth of the search. It can be 'basic' or 'advanced'", + default: "basic" + }, + topic : { + type: "string", + enum: ["general","news"], + description: "The category of the search. This will determine which of our agents will be used for the search", + default: "general" + }, + days: { + type: "number", + description: "The number of days back from the current date to include in the search results. This specifies the time frame of data to be retrieved. Please note that this feature is only available when using the 'news' search topic", + default: 3 + }, + time_range: { + type: "string", + description: "The time range back from the current date to include in the search results. This feature is available for both 'general' and 'news' search topics", + enum: ["day", "week", "month", "year", "d", "w", "m", "y"], + }, + max_results: { + type: "number", + description: "The maximum number of search results to return", + default: 10, + minimum: 5, + maximum: 20 + }, + include_images: { + type: "boolean", + description: "Include a list of query-related images in the response", + default: false, + }, + include_image_descriptions: { + type: "boolean", + description: "Include a list of query-related images and their descriptions in the response", + default: false, + }, + /* + // Since the mcp server is using claude to generate answers form the search results, we don't need to include this feature. + include_answer: { + type: ["boolean", "string"], + enum: [true, false, "basic", "advanced"], + description: "Include an answer to original query, generated by an LLM based on Tavily's search results. Can be boolean or string ('basic'/'advanced'). 'basic'/true answer will be quick but less detailed, 'advanced' answer will be more detailed but take longer to generate", + default: false, + }, + */ + include_raw_content: { + type: "boolean", + description: "Include the cleaned and parsed HTML content of each search result", + default: false, + }, + include_domains: { + type: "array", + items: { type: "string" }, + description: "A list of domains to specifically include in the search results, if the user asks to search on specific sites set this to the domain of the site", + default: [] + }, + exclude_domains: { + type: "array", + items: { type: "string" }, + description: "List of domains to specifically exclude, if the user asks to exclude a domain set this to the domain of the site", + default: [] + } + }, + required: ["query"] + } + }, + { + name: "tavily-extract", + description: "A powerful web content extraction tool that retrieves and processes raw content from specified URLs, ideal for data collection, content analysis, and research tasks.", + inputSchema: { + type: "object", + properties: { + urls: { + type: "array", + items: { type: "string" }, + description: "List of URLs to extract content from" + }, + extract_depth: { + type: "string", + enum: ["basic","advanced"], + description: "Depth of extraction - 'basic' or 'advanced', if usrls are linkedin use 'advanced' or if explicitly told to use advanced", + default: "basic" + }, + include_images: { + type: "boolean", + description: "Include a list of images extracted from the urls in the response", + default: false, + } + }, + required: ["urls"] + } + }, + ]; + return { tools }; + }); + + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + try { + let response: TavilyResponse; + const args = request.params.arguments ?? {}; + + switch (request.params.name) { + case "tavily-search": + response = await this.search({ + query: args.query, + search_depth: args.search_depth, + topic: args.topic, + days: args.days, + time_range: args.time_range, + max_results: args.max_results, + include_images: args.include_images, + include_image_descriptions: args.include_image_descriptions, + include_raw_content: args.include_raw_content, + include_domains: Array.isArray(args.include_domains) ? args.include_domains : [], + exclude_domains: Array.isArray(args.exclude_domains) ? args.exclude_domains : [] + }); + break; + + case "tavily-extract": + response = await this.extract({ + urls: args.urls, + extract_depth: args.extract_depth, + include_images: args.include_images + }); + break; + + default: + throw new McpError( + ErrorCode.MethodNotFound, + `Unknown tool: ${request.params.name}` + ); + } + + return { + content: [{ + type: "text", + text: formatResults(response) + }] + }; + } catch (error: any) { + if (axios.isAxiosError(error)) { + return { + content: [{ + type: "text", + text: `Tavily API error: ${error.response?.data?.message ?? error.message}` + }], + isError: true, + } + } + throw error; + } + }); + } + + + async run(): Promise { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error("Tavily MCP server running on stdio"); + } + + async search(params: any): Promise { + try { + // Choose endpoint based on whether it's an extract request + const endpoint = params.url ? this.baseURLs.extract : this.baseURLs.search; + + // Add topic: "news" if query contains the word "news" + const searchParams = { + ...params, + api_key: API_KEY, + topic: params.query.toLowerCase().includes('news') ? 'news' : undefined + }; + + const response = await this.axiosInstance.post(endpoint, searchParams); + return response.data; + } catch (error: any) { + if (error.response?.status === 401) { + throw new Error('Invalid API key'); + } else if (error.response?.status === 429) { + throw new Error('Usage limit exceeded'); + } + throw error; + } + } + + async extract(params: any): Promise { + try { + const response = await this.axiosInstance.post(this.baseURLs.extract, { + ...params, + api_key: API_KEY + }); + return response.data; + } catch (error: any) { + if (error.response?.status === 401) { + throw new Error('Invalid API key'); + } else if (error.response?.status === 429) { + throw new Error('Usage limit exceeded'); + } + throw error; + } + } +} + +function formatResults(response: TavilyResponse): string { + // Format API response into human-readable text + const output: string[] = []; + + // Include answer if available + if (response.answer) { + output.push(`Answer: ${response.answer}`); + output.push('\nSources:'); + response.results.forEach(result => { + output.push(`- ${result.title}: ${result.url}`); + }); + output.push(''); + } + + // Format detailed search results + output.push('Detailed Results:'); + response.results.forEach(result => { + output.push(`\nTitle: ${result.title}`); + output.push(`URL: ${result.url}`); + output.push(`Content: ${result.content}`); + if (result.raw_content) { + output.push(`Raw Content: ${result.raw_content}`); + } + }); + + return output.join('\n'); +} + +export async function serve(): Promise { + const client = new TavilyClient(); + await client.run(); +} + +const server = new TavilyClient(); +server.run().catch(console.error); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a14bee0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +}