├── .gitignore ├── .npmignore ├── Dockerfile ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── smithery.yaml ├── src ├── index.ts ├── tools │ ├── companyResearch.ts │ ├── competitorFinder.ts │ ├── config.ts │ ├── crawling.ts │ ├── githubSearch.ts │ ├── index.ts │ ├── linkedInSearch.ts │ ├── researchPaperSearch.ts │ ├── webSearch.ts │ └── wikipediaSearch.ts ├── types.ts └── utils │ └── logger.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | *.log 4 | .env* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | tests/ 3 | .github/ 4 | .gitignore 5 | .npmignore 6 | tsconfig.json 7 | *.log 8 | .env* -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Node.js 18 image as a parent image 2 | FROM node:18-alpine AS builder 3 | 4 | # Set the working directory in the container to /app 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json into the container 8 | COPY package.json package-lock.json ./ 9 | 10 | # Install dependencies 11 | RUN npm ci --ignore-scripts 12 | 13 | # Copy the rest of the application code into the container 14 | COPY src/ ./src/ 15 | COPY tsconfig.json ./ 16 | 17 | # Build the project 18 | RUN npm run build 19 | 20 | # Use a minimal node image as the base image for running 21 | FROM node:18-alpine AS runner 22 | 23 | WORKDIR /app 24 | 25 | # Copy compiled code from the builder stage 26 | COPY --from=builder /app/build ./build 27 | COPY package.json package-lock.json ./ 28 | 29 | # Install only production dependencies 30 | RUN npm ci --production --ignore-scripts 31 | 32 | # Set environment variable for the Exa API key 33 | ENV EXA_API_KEY=your-api-key-here 34 | 35 | # Expose the port the app runs on 36 | EXPOSE 3000 37 | 38 | # Run the application 39 | ENTRYPOINT ["node", "build/index.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Exa Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Exa MCP Server 🔍 2 | [![npm version](https://badge.fury.io/js/exa-mcp-server.svg)](https://www.npmjs.com/package/exa-mcp-server) 3 | [![smithery badge](https://smithery.ai/badge/exa)](https://smithery.ai/server/exa) 4 | 5 | A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way. 6 | 7 | ## Remote Exa MCP 🌐 8 | 9 | Connect directly to Exa's hosted MCP server (instead of running it locally). 10 | 11 | ### Remote Exa MCP URL 12 | 13 | ``` 14 | https://mcp.exa.ai/mcp?exaApiKey=your-exa-api-key 15 | ``` 16 | 17 | Replace `your-api-key-here` with your actual Exa API key from [dashboard.exa.ai/api-keys](https://dashboard.exa.ai/api-keys). 18 | 19 | ### Claude Desktop Configuration for Remote MCP 20 | 21 | Add this to your Claude Desktop configuration file: 22 | 23 | ```json 24 | { 25 | "mcpServers": { 26 | "exa": { 27 | "command": "npx", 28 | "args": [ 29 | "-y", 30 | "mcp-remote", 31 | "https://mcp.exa.ai/mcp?exaApiKey=your-exa-api-key" 32 | ] 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | ### NPM Installation 39 | 40 | ```bash 41 | npm install -g exa-mcp-server 42 | ``` 43 | 44 | ### Using Smithery 45 | 46 | To install the Exa MCP server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/exa): 47 | 48 | ```bash 49 | npx -y @smithery/cli install exa --client claude 50 | ``` 51 | 52 | ## Configuration ⚙️ 53 | 54 | ### 1. Configure Claude Desktop to recognize the Exa MCP server 55 | 56 | You can find claude_desktop_config.json inside the settings of Claude Desktop app: 57 | 58 | Open the Claude Desktop app and enable Developer Mode from the top-left menu bar. 59 | 60 | Once enabled, open Settings (also from the top-left menu bar) and navigate to the Developer Option, where you'll find the Edit Config button. Clicking it will open the claude_desktop_config.json file, allowing you to make the necessary edits. 61 | 62 | OR (if you want to open claude_desktop_config.json from terminal) 63 | 64 | #### For macOS: 65 | 66 | 1. Open your Claude Desktop configuration: 67 | 68 | ```bash 69 | code ~/Library/Application\ Support/Claude/claude_desktop_config.json 70 | ``` 71 | 72 | #### For Windows: 73 | 74 | 1. Open your Claude Desktop configuration: 75 | 76 | ```powershell 77 | code %APPDATA%\Claude\claude_desktop_config.json 78 | ``` 79 | 80 | ### 2. Add the Exa server configuration: 81 | 82 | ```json 83 | { 84 | "mcpServers": { 85 | "exa": { 86 | "command": "npx", 87 | "args": ["-y", "exa-mcp-server"], 88 | "env": { 89 | "EXA_API_KEY": "your-api-key-here" 90 | } 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | Replace `your-api-key-here` with your actual Exa API key from [dashboard.exa.ai/api-keys](https://dashboard.exa.ai/api-keys). 97 | 98 | ### 3. Available Tools & Tool Selection 99 | 100 | The Exa MCP server includes the following tools, which can be enabled by adding the `--tools`: 101 | 102 | - **web_search_exa**: Performs real-time web searches with optimized results and content extraction. 103 | - **research_paper_search**: Specialized search focused on academic papers and research content. 104 | - **company_research**: Comprehensive company research tool that crawls company websites to gather detailed information about businesses. 105 | - **crawling**: Extracts content from specific URLs, useful for reading articles, PDFs, or any web page when you have the exact URL. 106 | - **competitor_finder**: Identifies competitors of a company by searching for businesses offering similar products or services. 107 | - **linkedin_search**: Search LinkedIn for companies and people using Exa AI. Simply include company names, person names, or specific LinkedIn URLs in your query. 108 | - **wikipedia_search_exa**: Search and retrieve information from Wikipedia articles on specific topics, giving you accurate, structured knowledge from the world's largest encyclopedia. 109 | - **github_search**: Search GitHub repositories using Exa AI - performs real-time searches on GitHub.com to find relevant repositories, issues, and GitHub accounts. 110 | 111 | You can choose which tools to enable by adding the `--tools` parameter to your Claude Desktop configuration: 112 | 113 | #### Specify which tools to enable: 114 | 115 | ```json 116 | { 117 | "mcpServers": { 118 | "exa": { 119 | "command": "npx", 120 | "args": [ 121 | "-y", 122 | "exa-mcp-server", 123 | "--tools=web_search_exa,research_paper_search,company_research,crawling,competitor_finder,linkedin_search,wikipedia_search_exa,github_search" 124 | ], 125 | "env": { 126 | "EXA_API_KEY": "your-api-key-here" 127 | } 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | For enabling multiple tools, use a comma-separated list: 134 | 135 | ```json 136 | { 137 | "mcpServers": { 138 | "exa": { 139 | "command": "npx", 140 | "args": [ 141 | "-y", 142 | "exa-mcp-server", 143 | "--tools=web_search_exa,research_paper_search,company_research,crawling,competitor_finder,linkedin_search,wikipedia_search_exa,github_search" 144 | ], 145 | "env": { 146 | "EXA_API_KEY": "your-api-key-here" 147 | } 148 | } 149 | } 150 | } 151 | ``` 152 | 153 | If you don't specify any tools, all tools enabled by default will be used. 154 | 155 | ### 4. Restart Claude Desktop 156 | 157 | For the changes to take effect: 158 | 159 | 1. Completely quit Claude Desktop (not just close the window) 160 | 2. Start Claude Desktop again 161 | 3. Look for the icon to verify the Exa server is connected 162 | 163 | ## Using via NPX 164 | 165 | If you prefer to run the server directly, you can use npx: 166 | 167 | ```bash 168 | # Run with all tools enabled by default 169 | npx exa-mcp-server 170 | 171 | # Enable specific tools only 172 | npx exa-mcp-server --tools=web_search_exa 173 | 174 | # Enable multiple tools 175 | npx exa-mcp-server --tools=web_search_exa,research_paper_search 176 | 177 | # List all available tools 178 | npx exa-mcp-server --list-tools 179 | ``` 180 | 181 | ## Troubleshooting 🔧 182 | 183 | ### Common Issues 184 | 185 | 1. **Server Not Found** 186 | * Verify the npm link is correctly set up 187 | * Check Claude Desktop configuration syntax (json file) 188 | 189 | 2. **API Key Issues** 190 | * Confirm your EXA_API_KEY is valid 191 | * Check the EXA_API_KEY is correctly set in the Claude Desktop config 192 | * Verify no spaces or quotes around the API key 193 | 194 | 3. **Connection Issues** 195 | * Restart Claude Desktop completely 196 | * Check Claude Desktop logs: 197 | 198 |
199 | 200 | --- 201 | 202 | Built with ❤️ by team Exa -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exa-mcp-server", 3 | "version": "0.3.10", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "exa-mcp-server", 9 | "version": "0.3.10", 10 | "dependencies": { 11 | "@modelcontextprotocol/sdk": "^1.7.0", 12 | "axios": "^1.7.8", 13 | "dotenv": "^16.4.5", 14 | "yargs": "^17.7.2", 15 | "zod": "^3.22.4" 16 | }, 17 | "bin": { 18 | "exa-mcp-server": "build/index.js" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^20.11.24", 22 | "@types/yargs": "^17.0.33", 23 | "typescript": "^5.3.3" 24 | }, 25 | "engines": { 26 | "node": ">=18.0.0" 27 | } 28 | }, 29 | "node_modules/@modelcontextprotocol/sdk": { 30 | "version": "1.7.0", 31 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.7.0.tgz", 32 | "integrity": "sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA==", 33 | "dependencies": { 34 | "content-type": "^1.0.5", 35 | "cors": "^2.8.5", 36 | "eventsource": "^3.0.2", 37 | "express": "^5.0.1", 38 | "express-rate-limit": "^7.5.0", 39 | "pkce-challenge": "^4.1.0", 40 | "raw-body": "^3.0.0", 41 | "zod": "^3.23.8", 42 | "zod-to-json-schema": "^3.24.1" 43 | }, 44 | "engines": { 45 | "node": ">=18" 46 | } 47 | }, 48 | "node_modules/@types/node": { 49 | "version": "20.17.8", 50 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.8.tgz", 51 | "integrity": "sha512-ahz2g6/oqbKalW9sPv6L2iRbhLnojxjYWspAqhjvqSWBgGebEJT5GvRmk0QXPj3sbC6rU0GTQjPLQkmR8CObvA==", 52 | "dev": true, 53 | "dependencies": { 54 | "undici-types": "~6.19.2" 55 | } 56 | }, 57 | "node_modules/@types/yargs": { 58 | "version": "17.0.33", 59 | "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", 60 | "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", 61 | "dev": true, 62 | "dependencies": { 63 | "@types/yargs-parser": "*" 64 | } 65 | }, 66 | "node_modules/@types/yargs-parser": { 67 | "version": "21.0.3", 68 | "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", 69 | "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", 70 | "dev": true 71 | }, 72 | "node_modules/accepts": { 73 | "version": "2.0.0", 74 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", 75 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 76 | "dependencies": { 77 | "mime-types": "^3.0.0", 78 | "negotiator": "^1.0.0" 79 | }, 80 | "engines": { 81 | "node": ">= 0.6" 82 | } 83 | }, 84 | "node_modules/accepts/node_modules/mime-db": { 85 | "version": "1.54.0", 86 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 87 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 88 | "engines": { 89 | "node": ">= 0.6" 90 | } 91 | }, 92 | "node_modules/accepts/node_modules/mime-types": { 93 | "version": "3.0.0", 94 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", 95 | "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", 96 | "dependencies": { 97 | "mime-db": "^1.53.0" 98 | }, 99 | "engines": { 100 | "node": ">= 0.6" 101 | } 102 | }, 103 | "node_modules/ansi-regex": { 104 | "version": "5.0.1", 105 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 106 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 107 | "engines": { 108 | "node": ">=8" 109 | } 110 | }, 111 | "node_modules/ansi-styles": { 112 | "version": "4.3.0", 113 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 114 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 115 | "dependencies": { 116 | "color-convert": "^2.0.1" 117 | }, 118 | "engines": { 119 | "node": ">=8" 120 | }, 121 | "funding": { 122 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 123 | } 124 | }, 125 | "node_modules/asynckit": { 126 | "version": "0.4.0", 127 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 128 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 129 | }, 130 | "node_modules/axios": { 131 | "version": "1.7.8", 132 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", 133 | "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", 134 | "dependencies": { 135 | "follow-redirects": "^1.15.6", 136 | "form-data": "^4.0.0", 137 | "proxy-from-env": "^1.1.0" 138 | } 139 | }, 140 | "node_modules/body-parser": { 141 | "version": "2.1.0", 142 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.1.0.tgz", 143 | "integrity": "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==", 144 | "dependencies": { 145 | "bytes": "^3.1.2", 146 | "content-type": "^1.0.5", 147 | "debug": "^4.4.0", 148 | "http-errors": "^2.0.0", 149 | "iconv-lite": "^0.5.2", 150 | "on-finished": "^2.4.1", 151 | "qs": "^6.14.0", 152 | "raw-body": "^3.0.0", 153 | "type-is": "^2.0.0" 154 | }, 155 | "engines": { 156 | "node": ">=18" 157 | } 158 | }, 159 | "node_modules/body-parser/node_modules/debug": { 160 | "version": "4.4.0", 161 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 162 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 163 | "dependencies": { 164 | "ms": "^2.1.3" 165 | }, 166 | "engines": { 167 | "node": ">=6.0" 168 | }, 169 | "peerDependenciesMeta": { 170 | "supports-color": { 171 | "optional": true 172 | } 173 | } 174 | }, 175 | "node_modules/body-parser/node_modules/iconv-lite": { 176 | "version": "0.5.2", 177 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", 178 | "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", 179 | "dependencies": { 180 | "safer-buffer": ">= 2.1.2 < 3" 181 | }, 182 | "engines": { 183 | "node": ">=0.10.0" 184 | } 185 | }, 186 | "node_modules/body-parser/node_modules/ms": { 187 | "version": "2.1.3", 188 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 189 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 190 | }, 191 | "node_modules/body-parser/node_modules/qs": { 192 | "version": "6.14.0", 193 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 194 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 195 | "dependencies": { 196 | "side-channel": "^1.1.0" 197 | }, 198 | "engines": { 199 | "node": ">=0.6" 200 | }, 201 | "funding": { 202 | "url": "https://github.com/sponsors/ljharb" 203 | } 204 | }, 205 | "node_modules/bytes": { 206 | "version": "3.1.2", 207 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 208 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 209 | "engines": { 210 | "node": ">= 0.8" 211 | } 212 | }, 213 | "node_modules/call-bind-apply-helpers": { 214 | "version": "1.0.2", 215 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 216 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 217 | "dependencies": { 218 | "es-errors": "^1.3.0", 219 | "function-bind": "^1.1.2" 220 | }, 221 | "engines": { 222 | "node": ">= 0.4" 223 | } 224 | }, 225 | "node_modules/call-bound": { 226 | "version": "1.0.4", 227 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 228 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 229 | "dependencies": { 230 | "call-bind-apply-helpers": "^1.0.2", 231 | "get-intrinsic": "^1.3.0" 232 | }, 233 | "engines": { 234 | "node": ">= 0.4" 235 | }, 236 | "funding": { 237 | "url": "https://github.com/sponsors/ljharb" 238 | } 239 | }, 240 | "node_modules/cliui": { 241 | "version": "8.0.1", 242 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 243 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 244 | "dependencies": { 245 | "string-width": "^4.2.0", 246 | "strip-ansi": "^6.0.1", 247 | "wrap-ansi": "^7.0.0" 248 | }, 249 | "engines": { 250 | "node": ">=12" 251 | } 252 | }, 253 | "node_modules/color-convert": { 254 | "version": "2.0.1", 255 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 256 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 257 | "dependencies": { 258 | "color-name": "~1.1.4" 259 | }, 260 | "engines": { 261 | "node": ">=7.0.0" 262 | } 263 | }, 264 | "node_modules/color-name": { 265 | "version": "1.1.4", 266 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 267 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 268 | }, 269 | "node_modules/combined-stream": { 270 | "version": "1.0.8", 271 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 272 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 273 | "dependencies": { 274 | "delayed-stream": "~1.0.0" 275 | }, 276 | "engines": { 277 | "node": ">= 0.8" 278 | } 279 | }, 280 | "node_modules/content-disposition": { 281 | "version": "1.0.0", 282 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", 283 | "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 284 | "dependencies": { 285 | "safe-buffer": "5.2.1" 286 | }, 287 | "engines": { 288 | "node": ">= 0.6" 289 | } 290 | }, 291 | "node_modules/content-type": { 292 | "version": "1.0.5", 293 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 294 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 295 | "engines": { 296 | "node": ">= 0.6" 297 | } 298 | }, 299 | "node_modules/cookie": { 300 | "version": "0.7.1", 301 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 302 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 303 | "engines": { 304 | "node": ">= 0.6" 305 | } 306 | }, 307 | "node_modules/cookie-signature": { 308 | "version": "1.2.2", 309 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", 310 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 311 | "engines": { 312 | "node": ">=6.6.0" 313 | } 314 | }, 315 | "node_modules/cors": { 316 | "version": "2.8.5", 317 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 318 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 319 | "dependencies": { 320 | "object-assign": "^4", 321 | "vary": "^1" 322 | }, 323 | "engines": { 324 | "node": ">= 0.10" 325 | } 326 | }, 327 | "node_modules/debug": { 328 | "version": "4.3.6", 329 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", 330 | "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", 331 | "dependencies": { 332 | "ms": "2.1.2" 333 | }, 334 | "engines": { 335 | "node": ">=6.0" 336 | }, 337 | "peerDependenciesMeta": { 338 | "supports-color": { 339 | "optional": true 340 | } 341 | } 342 | }, 343 | "node_modules/delayed-stream": { 344 | "version": "1.0.0", 345 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 346 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 347 | "engines": { 348 | "node": ">=0.4.0" 349 | } 350 | }, 351 | "node_modules/depd": { 352 | "version": "2.0.0", 353 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 354 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 355 | "engines": { 356 | "node": ">= 0.8" 357 | } 358 | }, 359 | "node_modules/destroy": { 360 | "version": "1.2.0", 361 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 362 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 363 | "engines": { 364 | "node": ">= 0.8", 365 | "npm": "1.2.8000 || >= 1.4.16" 366 | } 367 | }, 368 | "node_modules/dotenv": { 369 | "version": "16.4.5", 370 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 371 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 372 | "engines": { 373 | "node": ">=12" 374 | }, 375 | "funding": { 376 | "url": "https://dotenvx.com" 377 | } 378 | }, 379 | "node_modules/dunder-proto": { 380 | "version": "1.0.1", 381 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 382 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 383 | "dependencies": { 384 | "call-bind-apply-helpers": "^1.0.1", 385 | "es-errors": "^1.3.0", 386 | "gopd": "^1.2.0" 387 | }, 388 | "engines": { 389 | "node": ">= 0.4" 390 | } 391 | }, 392 | "node_modules/ee-first": { 393 | "version": "1.1.1", 394 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 395 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 396 | }, 397 | "node_modules/emoji-regex": { 398 | "version": "8.0.0", 399 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 400 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 401 | }, 402 | "node_modules/encodeurl": { 403 | "version": "2.0.0", 404 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 405 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 406 | "engines": { 407 | "node": ">= 0.8" 408 | } 409 | }, 410 | "node_modules/es-define-property": { 411 | "version": "1.0.1", 412 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 413 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 414 | "engines": { 415 | "node": ">= 0.4" 416 | } 417 | }, 418 | "node_modules/es-errors": { 419 | "version": "1.3.0", 420 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 421 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 422 | "engines": { 423 | "node": ">= 0.4" 424 | } 425 | }, 426 | "node_modules/es-object-atoms": { 427 | "version": "1.1.1", 428 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 429 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 430 | "dependencies": { 431 | "es-errors": "^1.3.0" 432 | }, 433 | "engines": { 434 | "node": ">= 0.4" 435 | } 436 | }, 437 | "node_modules/escalade": { 438 | "version": "3.2.0", 439 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 440 | "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 441 | "engines": { 442 | "node": ">=6" 443 | } 444 | }, 445 | "node_modules/escape-html": { 446 | "version": "1.0.3", 447 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 448 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 449 | }, 450 | "node_modules/etag": { 451 | "version": "1.8.1", 452 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 453 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 454 | "engines": { 455 | "node": ">= 0.6" 456 | } 457 | }, 458 | "node_modules/eventsource": { 459 | "version": "3.0.5", 460 | "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.5.tgz", 461 | "integrity": "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==", 462 | "dependencies": { 463 | "eventsource-parser": "^3.0.0" 464 | }, 465 | "engines": { 466 | "node": ">=18.0.0" 467 | } 468 | }, 469 | "node_modules/eventsource-parser": { 470 | "version": "3.0.0", 471 | "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", 472 | "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", 473 | "engines": { 474 | "node": ">=18.0.0" 475 | } 476 | }, 477 | "node_modules/express": { 478 | "version": "5.0.1", 479 | "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", 480 | "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", 481 | "dependencies": { 482 | "accepts": "^2.0.0", 483 | "body-parser": "^2.0.1", 484 | "content-disposition": "^1.0.0", 485 | "content-type": "~1.0.4", 486 | "cookie": "0.7.1", 487 | "cookie-signature": "^1.2.1", 488 | "debug": "4.3.6", 489 | "depd": "2.0.0", 490 | "encodeurl": "~2.0.0", 491 | "escape-html": "~1.0.3", 492 | "etag": "~1.8.1", 493 | "finalhandler": "^2.0.0", 494 | "fresh": "2.0.0", 495 | "http-errors": "2.0.0", 496 | "merge-descriptors": "^2.0.0", 497 | "methods": "~1.1.2", 498 | "mime-types": "^3.0.0", 499 | "on-finished": "2.4.1", 500 | "once": "1.4.0", 501 | "parseurl": "~1.3.3", 502 | "proxy-addr": "~2.0.7", 503 | "qs": "6.13.0", 504 | "range-parser": "~1.2.1", 505 | "router": "^2.0.0", 506 | "safe-buffer": "5.2.1", 507 | "send": "^1.1.0", 508 | "serve-static": "^2.1.0", 509 | "setprototypeof": "1.2.0", 510 | "statuses": "2.0.1", 511 | "type-is": "^2.0.0", 512 | "utils-merge": "1.0.1", 513 | "vary": "~1.1.2" 514 | }, 515 | "engines": { 516 | "node": ">= 18" 517 | } 518 | }, 519 | "node_modules/express-rate-limit": { 520 | "version": "7.5.0", 521 | "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", 522 | "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", 523 | "engines": { 524 | "node": ">= 16" 525 | }, 526 | "funding": { 527 | "url": "https://github.com/sponsors/express-rate-limit" 528 | }, 529 | "peerDependencies": { 530 | "express": "^4.11 || 5 || ^5.0.0-beta.1" 531 | } 532 | }, 533 | "node_modules/express/node_modules/mime-db": { 534 | "version": "1.54.0", 535 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 536 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 537 | "engines": { 538 | "node": ">= 0.6" 539 | } 540 | }, 541 | "node_modules/express/node_modules/mime-types": { 542 | "version": "3.0.0", 543 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", 544 | "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", 545 | "dependencies": { 546 | "mime-db": "^1.53.0" 547 | }, 548 | "engines": { 549 | "node": ">= 0.6" 550 | } 551 | }, 552 | "node_modules/finalhandler": { 553 | "version": "2.1.0", 554 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", 555 | "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 556 | "dependencies": { 557 | "debug": "^4.4.0", 558 | "encodeurl": "^2.0.0", 559 | "escape-html": "^1.0.3", 560 | "on-finished": "^2.4.1", 561 | "parseurl": "^1.3.3", 562 | "statuses": "^2.0.1" 563 | }, 564 | "engines": { 565 | "node": ">= 0.8" 566 | } 567 | }, 568 | "node_modules/finalhandler/node_modules/debug": { 569 | "version": "4.4.0", 570 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 571 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 572 | "dependencies": { 573 | "ms": "^2.1.3" 574 | }, 575 | "engines": { 576 | "node": ">=6.0" 577 | }, 578 | "peerDependenciesMeta": { 579 | "supports-color": { 580 | "optional": true 581 | } 582 | } 583 | }, 584 | "node_modules/finalhandler/node_modules/ms": { 585 | "version": "2.1.3", 586 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 587 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 588 | }, 589 | "node_modules/follow-redirects": { 590 | "version": "1.15.9", 591 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", 592 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 593 | "funding": [ 594 | { 595 | "type": "individual", 596 | "url": "https://github.com/sponsors/RubenVerborgh" 597 | } 598 | ], 599 | "engines": { 600 | "node": ">=4.0" 601 | }, 602 | "peerDependenciesMeta": { 603 | "debug": { 604 | "optional": true 605 | } 606 | } 607 | }, 608 | "node_modules/form-data": { 609 | "version": "4.0.1", 610 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", 611 | "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", 612 | "dependencies": { 613 | "asynckit": "^0.4.0", 614 | "combined-stream": "^1.0.8", 615 | "mime-types": "^2.1.12" 616 | }, 617 | "engines": { 618 | "node": ">= 6" 619 | } 620 | }, 621 | "node_modules/forwarded": { 622 | "version": "0.2.0", 623 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 624 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 625 | "engines": { 626 | "node": ">= 0.6" 627 | } 628 | }, 629 | "node_modules/fresh": { 630 | "version": "2.0.0", 631 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", 632 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 633 | "engines": { 634 | "node": ">= 0.8" 635 | } 636 | }, 637 | "node_modules/function-bind": { 638 | "version": "1.1.2", 639 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 640 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 641 | "funding": { 642 | "url": "https://github.com/sponsors/ljharb" 643 | } 644 | }, 645 | "node_modules/get-caller-file": { 646 | "version": "2.0.5", 647 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 648 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 649 | "engines": { 650 | "node": "6.* || 8.* || >= 10.*" 651 | } 652 | }, 653 | "node_modules/get-intrinsic": { 654 | "version": "1.3.0", 655 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 656 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 657 | "dependencies": { 658 | "call-bind-apply-helpers": "^1.0.2", 659 | "es-define-property": "^1.0.1", 660 | "es-errors": "^1.3.0", 661 | "es-object-atoms": "^1.1.1", 662 | "function-bind": "^1.1.2", 663 | "get-proto": "^1.0.1", 664 | "gopd": "^1.2.0", 665 | "has-symbols": "^1.1.0", 666 | "hasown": "^2.0.2", 667 | "math-intrinsics": "^1.1.0" 668 | }, 669 | "engines": { 670 | "node": ">= 0.4" 671 | }, 672 | "funding": { 673 | "url": "https://github.com/sponsors/ljharb" 674 | } 675 | }, 676 | "node_modules/get-proto": { 677 | "version": "1.0.1", 678 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 679 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 680 | "dependencies": { 681 | "dunder-proto": "^1.0.1", 682 | "es-object-atoms": "^1.0.0" 683 | }, 684 | "engines": { 685 | "node": ">= 0.4" 686 | } 687 | }, 688 | "node_modules/gopd": { 689 | "version": "1.2.0", 690 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 691 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 692 | "engines": { 693 | "node": ">= 0.4" 694 | }, 695 | "funding": { 696 | "url": "https://github.com/sponsors/ljharb" 697 | } 698 | }, 699 | "node_modules/has-symbols": { 700 | "version": "1.1.0", 701 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 702 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 703 | "engines": { 704 | "node": ">= 0.4" 705 | }, 706 | "funding": { 707 | "url": "https://github.com/sponsors/ljharb" 708 | } 709 | }, 710 | "node_modules/hasown": { 711 | "version": "2.0.2", 712 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 713 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 714 | "dependencies": { 715 | "function-bind": "^1.1.2" 716 | }, 717 | "engines": { 718 | "node": ">= 0.4" 719 | } 720 | }, 721 | "node_modules/http-errors": { 722 | "version": "2.0.0", 723 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 724 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 725 | "dependencies": { 726 | "depd": "2.0.0", 727 | "inherits": "2.0.4", 728 | "setprototypeof": "1.2.0", 729 | "statuses": "2.0.1", 730 | "toidentifier": "1.0.1" 731 | }, 732 | "engines": { 733 | "node": ">= 0.8" 734 | } 735 | }, 736 | "node_modules/iconv-lite": { 737 | "version": "0.6.3", 738 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 739 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 740 | "dependencies": { 741 | "safer-buffer": ">= 2.1.2 < 3.0.0" 742 | }, 743 | "engines": { 744 | "node": ">=0.10.0" 745 | } 746 | }, 747 | "node_modules/inherits": { 748 | "version": "2.0.4", 749 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 750 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 751 | }, 752 | "node_modules/ipaddr.js": { 753 | "version": "1.9.1", 754 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 755 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 756 | "engines": { 757 | "node": ">= 0.10" 758 | } 759 | }, 760 | "node_modules/is-fullwidth-code-point": { 761 | "version": "3.0.0", 762 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 763 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 764 | "engines": { 765 | "node": ">=8" 766 | } 767 | }, 768 | "node_modules/is-promise": { 769 | "version": "4.0.0", 770 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", 771 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" 772 | }, 773 | "node_modules/math-intrinsics": { 774 | "version": "1.1.0", 775 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 776 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 777 | "engines": { 778 | "node": ">= 0.4" 779 | } 780 | }, 781 | "node_modules/media-typer": { 782 | "version": "1.1.0", 783 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", 784 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 785 | "engines": { 786 | "node": ">= 0.8" 787 | } 788 | }, 789 | "node_modules/merge-descriptors": { 790 | "version": "2.0.0", 791 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 792 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 793 | "engines": { 794 | "node": ">=18" 795 | }, 796 | "funding": { 797 | "url": "https://github.com/sponsors/sindresorhus" 798 | } 799 | }, 800 | "node_modules/methods": { 801 | "version": "1.1.2", 802 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 803 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 804 | "engines": { 805 | "node": ">= 0.6" 806 | } 807 | }, 808 | "node_modules/mime-db": { 809 | "version": "1.52.0", 810 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 811 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 812 | "engines": { 813 | "node": ">= 0.6" 814 | } 815 | }, 816 | "node_modules/mime-types": { 817 | "version": "2.1.35", 818 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 819 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 820 | "dependencies": { 821 | "mime-db": "1.52.0" 822 | }, 823 | "engines": { 824 | "node": ">= 0.6" 825 | } 826 | }, 827 | "node_modules/ms": { 828 | "version": "2.1.2", 829 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 830 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 831 | }, 832 | "node_modules/negotiator": { 833 | "version": "1.0.0", 834 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 835 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 836 | "engines": { 837 | "node": ">= 0.6" 838 | } 839 | }, 840 | "node_modules/object-assign": { 841 | "version": "4.1.1", 842 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 843 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 844 | "engines": { 845 | "node": ">=0.10.0" 846 | } 847 | }, 848 | "node_modules/object-inspect": { 849 | "version": "1.13.4", 850 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 851 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 852 | "engines": { 853 | "node": ">= 0.4" 854 | }, 855 | "funding": { 856 | "url": "https://github.com/sponsors/ljharb" 857 | } 858 | }, 859 | "node_modules/on-finished": { 860 | "version": "2.4.1", 861 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 862 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 863 | "dependencies": { 864 | "ee-first": "1.1.1" 865 | }, 866 | "engines": { 867 | "node": ">= 0.8" 868 | } 869 | }, 870 | "node_modules/once": { 871 | "version": "1.4.0", 872 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 873 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 874 | "dependencies": { 875 | "wrappy": "1" 876 | } 877 | }, 878 | "node_modules/parseurl": { 879 | "version": "1.3.3", 880 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 881 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 882 | "engines": { 883 | "node": ">= 0.8" 884 | } 885 | }, 886 | "node_modules/path-to-regexp": { 887 | "version": "8.2.0", 888 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", 889 | "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", 890 | "engines": { 891 | "node": ">=16" 892 | } 893 | }, 894 | "node_modules/pkce-challenge": { 895 | "version": "4.1.0", 896 | "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", 897 | "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", 898 | "engines": { 899 | "node": ">=16.20.0" 900 | } 901 | }, 902 | "node_modules/proxy-addr": { 903 | "version": "2.0.7", 904 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 905 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 906 | "dependencies": { 907 | "forwarded": "0.2.0", 908 | "ipaddr.js": "1.9.1" 909 | }, 910 | "engines": { 911 | "node": ">= 0.10" 912 | } 913 | }, 914 | "node_modules/proxy-from-env": { 915 | "version": "1.1.0", 916 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 917 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 918 | }, 919 | "node_modules/qs": { 920 | "version": "6.13.0", 921 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 922 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 923 | "dependencies": { 924 | "side-channel": "^1.0.6" 925 | }, 926 | "engines": { 927 | "node": ">=0.6" 928 | }, 929 | "funding": { 930 | "url": "https://github.com/sponsors/ljharb" 931 | } 932 | }, 933 | "node_modules/range-parser": { 934 | "version": "1.2.1", 935 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 936 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 937 | "engines": { 938 | "node": ">= 0.6" 939 | } 940 | }, 941 | "node_modules/raw-body": { 942 | "version": "3.0.0", 943 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 944 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 945 | "dependencies": { 946 | "bytes": "3.1.2", 947 | "http-errors": "2.0.0", 948 | "iconv-lite": "0.6.3", 949 | "unpipe": "1.0.0" 950 | }, 951 | "engines": { 952 | "node": ">= 0.8" 953 | } 954 | }, 955 | "node_modules/require-directory": { 956 | "version": "2.1.1", 957 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 958 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 959 | "engines": { 960 | "node": ">=0.10.0" 961 | } 962 | }, 963 | "node_modules/router": { 964 | "version": "2.1.0", 965 | "resolved": "https://registry.npmjs.org/router/-/router-2.1.0.tgz", 966 | "integrity": "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==", 967 | "dependencies": { 968 | "is-promise": "^4.0.0", 969 | "parseurl": "^1.3.3", 970 | "path-to-regexp": "^8.0.0" 971 | }, 972 | "engines": { 973 | "node": ">= 18" 974 | } 975 | }, 976 | "node_modules/safe-buffer": { 977 | "version": "5.2.1", 978 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 979 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 980 | "funding": [ 981 | { 982 | "type": "github", 983 | "url": "https://github.com/sponsors/feross" 984 | }, 985 | { 986 | "type": "patreon", 987 | "url": "https://www.patreon.com/feross" 988 | }, 989 | { 990 | "type": "consulting", 991 | "url": "https://feross.org/support" 992 | } 993 | ] 994 | }, 995 | "node_modules/safer-buffer": { 996 | "version": "2.1.2", 997 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 998 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 999 | }, 1000 | "node_modules/send": { 1001 | "version": "1.1.0", 1002 | "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", 1003 | "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", 1004 | "dependencies": { 1005 | "debug": "^4.3.5", 1006 | "destroy": "^1.2.0", 1007 | "encodeurl": "^2.0.0", 1008 | "escape-html": "^1.0.3", 1009 | "etag": "^1.8.1", 1010 | "fresh": "^0.5.2", 1011 | "http-errors": "^2.0.0", 1012 | "mime-types": "^2.1.35", 1013 | "ms": "^2.1.3", 1014 | "on-finished": "^2.4.1", 1015 | "range-parser": "^1.2.1", 1016 | "statuses": "^2.0.1" 1017 | }, 1018 | "engines": { 1019 | "node": ">= 18" 1020 | } 1021 | }, 1022 | "node_modules/send/node_modules/fresh": { 1023 | "version": "0.5.2", 1024 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 1025 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 1026 | "engines": { 1027 | "node": ">= 0.6" 1028 | } 1029 | }, 1030 | "node_modules/send/node_modules/ms": { 1031 | "version": "2.1.3", 1032 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1033 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 1034 | }, 1035 | "node_modules/serve-static": { 1036 | "version": "2.1.0", 1037 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", 1038 | "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", 1039 | "dependencies": { 1040 | "encodeurl": "^2.0.0", 1041 | "escape-html": "^1.0.3", 1042 | "parseurl": "^1.3.3", 1043 | "send": "^1.0.0" 1044 | }, 1045 | "engines": { 1046 | "node": ">= 18" 1047 | } 1048 | }, 1049 | "node_modules/setprototypeof": { 1050 | "version": "1.2.0", 1051 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1052 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1053 | }, 1054 | "node_modules/side-channel": { 1055 | "version": "1.1.0", 1056 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 1057 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 1058 | "dependencies": { 1059 | "es-errors": "^1.3.0", 1060 | "object-inspect": "^1.13.3", 1061 | "side-channel-list": "^1.0.0", 1062 | "side-channel-map": "^1.0.1", 1063 | "side-channel-weakmap": "^1.0.2" 1064 | }, 1065 | "engines": { 1066 | "node": ">= 0.4" 1067 | }, 1068 | "funding": { 1069 | "url": "https://github.com/sponsors/ljharb" 1070 | } 1071 | }, 1072 | "node_modules/side-channel-list": { 1073 | "version": "1.0.0", 1074 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 1075 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 1076 | "dependencies": { 1077 | "es-errors": "^1.3.0", 1078 | "object-inspect": "^1.13.3" 1079 | }, 1080 | "engines": { 1081 | "node": ">= 0.4" 1082 | }, 1083 | "funding": { 1084 | "url": "https://github.com/sponsors/ljharb" 1085 | } 1086 | }, 1087 | "node_modules/side-channel-map": { 1088 | "version": "1.0.1", 1089 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 1090 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 1091 | "dependencies": { 1092 | "call-bound": "^1.0.2", 1093 | "es-errors": "^1.3.0", 1094 | "get-intrinsic": "^1.2.5", 1095 | "object-inspect": "^1.13.3" 1096 | }, 1097 | "engines": { 1098 | "node": ">= 0.4" 1099 | }, 1100 | "funding": { 1101 | "url": "https://github.com/sponsors/ljharb" 1102 | } 1103 | }, 1104 | "node_modules/side-channel-weakmap": { 1105 | "version": "1.0.2", 1106 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 1107 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 1108 | "dependencies": { 1109 | "call-bound": "^1.0.2", 1110 | "es-errors": "^1.3.0", 1111 | "get-intrinsic": "^1.2.5", 1112 | "object-inspect": "^1.13.3", 1113 | "side-channel-map": "^1.0.1" 1114 | }, 1115 | "engines": { 1116 | "node": ">= 0.4" 1117 | }, 1118 | "funding": { 1119 | "url": "https://github.com/sponsors/ljharb" 1120 | } 1121 | }, 1122 | "node_modules/statuses": { 1123 | "version": "2.0.1", 1124 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1125 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1126 | "engines": { 1127 | "node": ">= 0.8" 1128 | } 1129 | }, 1130 | "node_modules/string-width": { 1131 | "version": "4.2.3", 1132 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1133 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1134 | "dependencies": { 1135 | "emoji-regex": "^8.0.0", 1136 | "is-fullwidth-code-point": "^3.0.0", 1137 | "strip-ansi": "^6.0.1" 1138 | }, 1139 | "engines": { 1140 | "node": ">=8" 1141 | } 1142 | }, 1143 | "node_modules/strip-ansi": { 1144 | "version": "6.0.1", 1145 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1146 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1147 | "dependencies": { 1148 | "ansi-regex": "^5.0.1" 1149 | }, 1150 | "engines": { 1151 | "node": ">=8" 1152 | } 1153 | }, 1154 | "node_modules/toidentifier": { 1155 | "version": "1.0.1", 1156 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1157 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1158 | "engines": { 1159 | "node": ">=0.6" 1160 | } 1161 | }, 1162 | "node_modules/type-is": { 1163 | "version": "2.0.0", 1164 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", 1165 | "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", 1166 | "dependencies": { 1167 | "content-type": "^1.0.5", 1168 | "media-typer": "^1.1.0", 1169 | "mime-types": "^3.0.0" 1170 | }, 1171 | "engines": { 1172 | "node": ">= 0.6" 1173 | } 1174 | }, 1175 | "node_modules/type-is/node_modules/mime-db": { 1176 | "version": "1.54.0", 1177 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 1178 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 1179 | "engines": { 1180 | "node": ">= 0.6" 1181 | } 1182 | }, 1183 | "node_modules/type-is/node_modules/mime-types": { 1184 | "version": "3.0.0", 1185 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", 1186 | "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", 1187 | "dependencies": { 1188 | "mime-db": "^1.53.0" 1189 | }, 1190 | "engines": { 1191 | "node": ">= 0.6" 1192 | } 1193 | }, 1194 | "node_modules/typescript": { 1195 | "version": "5.7.2", 1196 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", 1197 | "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", 1198 | "dev": true, 1199 | "bin": { 1200 | "tsc": "bin/tsc", 1201 | "tsserver": "bin/tsserver" 1202 | }, 1203 | "engines": { 1204 | "node": ">=14.17" 1205 | } 1206 | }, 1207 | "node_modules/undici-types": { 1208 | "version": "6.19.8", 1209 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 1210 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 1211 | "dev": true 1212 | }, 1213 | "node_modules/unpipe": { 1214 | "version": "1.0.0", 1215 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1216 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1217 | "engines": { 1218 | "node": ">= 0.8" 1219 | } 1220 | }, 1221 | "node_modules/utils-merge": { 1222 | "version": "1.0.1", 1223 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1224 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1225 | "engines": { 1226 | "node": ">= 0.4.0" 1227 | } 1228 | }, 1229 | "node_modules/vary": { 1230 | "version": "1.1.2", 1231 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1232 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1233 | "engines": { 1234 | "node": ">= 0.8" 1235 | } 1236 | }, 1237 | "node_modules/wrap-ansi": { 1238 | "version": "7.0.0", 1239 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1240 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1241 | "dependencies": { 1242 | "ansi-styles": "^4.0.0", 1243 | "string-width": "^4.1.0", 1244 | "strip-ansi": "^6.0.0" 1245 | }, 1246 | "engines": { 1247 | "node": ">=10" 1248 | }, 1249 | "funding": { 1250 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1251 | } 1252 | }, 1253 | "node_modules/wrappy": { 1254 | "version": "1.0.2", 1255 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1256 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 1257 | }, 1258 | "node_modules/y18n": { 1259 | "version": "5.0.8", 1260 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1261 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1262 | "engines": { 1263 | "node": ">=10" 1264 | } 1265 | }, 1266 | "node_modules/yargs": { 1267 | "version": "17.7.2", 1268 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 1269 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 1270 | "dependencies": { 1271 | "cliui": "^8.0.1", 1272 | "escalade": "^3.1.1", 1273 | "get-caller-file": "^2.0.5", 1274 | "require-directory": "^2.1.1", 1275 | "string-width": "^4.2.3", 1276 | "y18n": "^5.0.5", 1277 | "yargs-parser": "^21.1.1" 1278 | }, 1279 | "engines": { 1280 | "node": ">=12" 1281 | } 1282 | }, 1283 | "node_modules/yargs-parser": { 1284 | "version": "21.1.1", 1285 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 1286 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 1287 | "engines": { 1288 | "node": ">=12" 1289 | } 1290 | }, 1291 | "node_modules/zod": { 1292 | "version": "3.24.2", 1293 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", 1294 | "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", 1295 | "funding": { 1296 | "url": "https://github.com/sponsors/colinhacks" 1297 | } 1298 | }, 1299 | "node_modules/zod-to-json-schema": { 1300 | "version": "3.24.4", 1301 | "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.4.tgz", 1302 | "integrity": "sha512-0uNlcvgabyrni9Ag8Vghj21drk7+7tp7VTwwR7KxxXXc/3pbXz2PHlDgj3cICahgF1kHm4dExBFj7BXrZJXzig==", 1303 | "peerDependencies": { 1304 | "zod": "^3.24.1" 1305 | } 1306 | } 1307 | } 1308 | } 1309 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exa-mcp-server", 3 | "version": "0.3.10", 4 | "description": "A Model Context Protocol server with Exa for web search, academic paper search, and Twitter/X.com search. Provides real-time web searches with configurable tool selection, allowing users to enable or disable specific search capabilities. Supports customizable result counts, live crawling options, and returns content from the most relevant websites.", 5 | "type": "module", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/exa-labs/exa-mcp-server.git" 9 | }, 10 | "bin": { 11 | "exa-mcp-server": "./build/index.js" 12 | }, 13 | "files": [ 14 | "build" 15 | ], 16 | "keywords": [ 17 | "mcp", 18 | "model context protocol", 19 | "exa", 20 | "websearch", 21 | "claude", 22 | "ai", 23 | "research", 24 | "papers", 25 | "linkedin" 26 | ], 27 | "author": "Exa Labs", 28 | "scripts": { 29 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 30 | "prepare": "npm run build", 31 | "watch": "tsc --watch", 32 | "inspector": "npx @modelcontextprotocol/inspector build/index.js", 33 | "prepublishOnly": "npm run build" 34 | }, 35 | "dependencies": { 36 | "@modelcontextprotocol/sdk": "^1.7.0", 37 | "axios": "^1.7.8", 38 | "dotenv": "^16.4.5", 39 | "yargs": "^17.7.2", 40 | "zod": "^3.22.4" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^20.11.24", 44 | "@types/yargs": "^17.0.33", 45 | "typescript": "^5.3.3" 46 | }, 47 | "engines": { 48 | "node": ">=18.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/deployments 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | properties: 9 | exaApiKey: 10 | type: string 11 | description: The API key for accessing the Exa Search API. 12 | commandFunction: 13 | # A function that produces the CLI command to start the MCP on stdio. 14 | |- 15 | (config) => { 16 | const env = {} 17 | if (config.exaApiKey) { 18 | env.EXA_API_KEY = config.exaApiKey 19 | } 20 | return { 21 | command: 'node', 22 | args: [ 23 | 'build/index.js', 24 | '--tools=web_search_exa,research_paper_search,company_research,crawling,competitor_finder,linkedin_search,wikipedia_search_exa,github_search' 25 | ], 26 | env 27 | } 28 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import dotenv from "dotenv"; 5 | import yargs from 'yargs'; 6 | import { hideBin } from 'yargs/helpers'; 7 | 8 | // Import the tool registry system 9 | import { toolRegistry } from "./tools/index.js"; 10 | import { log } from "./utils/logger.js"; 11 | 12 | dotenv.config(); 13 | 14 | // Parse command line arguments to determine which tools to enable 15 | const argv = yargs(hideBin(process.argv)) 16 | .option('tools', { 17 | type: 'string', 18 | description: 'Comma-separated list of tools to enable (if not specified, all enabled-by-default tools are used)', 19 | default: '' 20 | }) 21 | .option('list-tools', { 22 | type: 'boolean', 23 | description: 'List all available tools and exit', 24 | default: false 25 | }) 26 | .help() 27 | .argv; 28 | 29 | // Convert comma-separated string to Set for easier lookups 30 | const argvObj = argv as any; 31 | const toolsString = argvObj['tools'] || ''; 32 | const specifiedTools = new Set( 33 | toolsString ? toolsString.split(',').map((tool: string) => tool.trim()) : [] 34 | ); 35 | 36 | // List all available tools if requested 37 | if (argvObj['list-tools']) { 38 | console.log("Available tools:"); 39 | 40 | Object.entries(toolRegistry).forEach(([id, tool]) => { 41 | console.log(`- ${id}: ${tool.name}`); 42 | console.log(` Description: ${tool.description}`); 43 | console.log(` Enabled by default: ${tool.enabled ? 'Yes' : 'No'}`); 44 | console.log(); 45 | }); 46 | 47 | process.exit(0); 48 | } 49 | 50 | // Check for API key after handling list-tools to allow listing without a key 51 | const API_KEY = process.env.EXA_API_KEY; 52 | if (!API_KEY) { 53 | throw new Error("EXA_API_KEY environment variable is required"); 54 | } 55 | 56 | /** 57 | * Exa AI Web Search MCP Server 58 | * 59 | * This MCP server integrates Exa AI's search capabilities with Claude and other MCP-compatible clients. 60 | * Exa is a search engine and API specifically designed for up-to-date web searching and retrieval, 61 | * offering more recent and comprehensive results than what might be available in an LLM's training data. 62 | * 63 | * The server provides tools that enable: 64 | * - Real-time web searching with configurable parameters 65 | * - Research paper searches 66 | * - And more to come! 67 | */ 68 | 69 | class ExaServer { 70 | private server: McpServer; 71 | 72 | constructor() { 73 | this.server = new McpServer({ 74 | name: "exa-search-server", 75 | version: "0.3.10" 76 | }); 77 | 78 | log("Server initialized"); 79 | } 80 | 81 | private setupTools(): string[] { 82 | // Register tools based on specifications 83 | const registeredTools: string[] = []; 84 | 85 | Object.entries(toolRegistry).forEach(([toolId, tool]) => { 86 | // If specific tools were provided, only enable those. 87 | // Otherwise, enable all tools marked as enabled by default 88 | const shouldRegister = specifiedTools.size > 0 89 | ? specifiedTools.has(toolId) 90 | : tool.enabled; 91 | 92 | if (shouldRegister) { 93 | this.server.tool( 94 | tool.name, 95 | tool.description, 96 | tool.schema, 97 | tool.handler 98 | ); 99 | registeredTools.push(toolId); 100 | } 101 | }); 102 | 103 | return registeredTools; 104 | } 105 | 106 | async run(): Promise { 107 | try { 108 | // Set up tools before connecting 109 | const registeredTools = this.setupTools(); 110 | 111 | log(`Starting Exa MCP server with ${registeredTools.length} tools: ${registeredTools.join(', ')}`); 112 | 113 | const transport = new StdioServerTransport(); 114 | 115 | // Handle connection errors 116 | transport.onerror = (error) => { 117 | log(`Transport error: ${error.message}`); 118 | }; 119 | 120 | await this.server.connect(transport); 121 | log("Exa Search MCP server running on stdio"); 122 | } catch (error) { 123 | log(`Server initialization error: ${error instanceof Error ? error.message : String(error)}`); 124 | throw error; 125 | } 126 | } 127 | } 128 | 129 | // Create and run the server with proper error handling 130 | (async () => { 131 | try { 132 | const server = new ExaServer(); 133 | await server.run(); 134 | } catch (error) { 135 | log(`Fatal server error: ${error instanceof Error ? error.message : String(error)}`); 136 | process.exit(1); 137 | } 138 | })(); -------------------------------------------------------------------------------- /src/tools/companyResearch.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import axios from "axios"; 3 | import { toolRegistry, API_CONFIG } from "./config.js"; 4 | import { ExaSearchRequest, ExaSearchResponse } from "../types.js"; 5 | import { createRequestLogger } from "../utils/logger.js"; 6 | 7 | // Register the company research tool 8 | toolRegistry["company_research"] = { 9 | name: "company_research", 10 | description: "Research companies using Exa AI - performs targeted searches of company websites to gather comprehensive information about businesses. Returns detailed information from company websites including about pages, pricing, FAQs, blogs, and other relevant content. Specify the company URL and optionally target specific sections of their website.", 11 | schema: { 12 | query: z.string().describe("Company website URL (e.g., 'exa.ai' or 'https://exa.ai')"), 13 | subpages: z.number().optional().describe("Number of subpages to crawl (default: 10)"), 14 | subpageTarget: z.array(z.string()).optional().describe("Specific sections to target (e.g., ['about', 'pricing', 'faq', 'blog']). If not provided, will crawl the most relevant pages.") 15 | }, 16 | handler: async ({ query, numResults, subpages, subpageTarget }, extra) => { 17 | const requestId = `company_research-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; 18 | const logger = createRequestLogger(requestId, 'company_research'); 19 | 20 | logger.start(query); 21 | 22 | try { 23 | // Create a fresh axios instance for each request 24 | const axiosInstance = axios.create({ 25 | baseURL: API_CONFIG.BASE_URL, 26 | headers: { 27 | 'accept': 'application/json', 28 | 'content-type': 'application/json', 29 | 'x-api-key': process.env.EXA_API_KEY || '' 30 | }, 31 | timeout: 25000 32 | }); 33 | 34 | // Extract domain from query if it's a URL 35 | let domain = query; 36 | if (query.includes('http')) { 37 | try { 38 | const url = new URL(query); 39 | domain = url.hostname.replace('www.', ''); 40 | } catch (e) { 41 | logger.log(`Warning: Could not parse URL from query: ${query}`); 42 | } 43 | } 44 | 45 | const searchRequest: ExaSearchRequest = { 46 | query, 47 | category: "company", 48 | includeDomains: [query], 49 | type: "auto", 50 | numResults: 1, 51 | contents: { 52 | text: { 53 | maxCharacters: API_CONFIG.DEFAULT_MAX_CHARACTERS 54 | }, 55 | livecrawl: 'always', 56 | subpages: subpages || 10, 57 | } 58 | }; 59 | 60 | // Add subpage targets if provided 61 | if (subpageTarget && subpageTarget.length > 0) { 62 | searchRequest.contents.subpageTarget = subpageTarget; 63 | logger.log(`Targeting specific sections: ${subpageTarget.join(', ')}`); 64 | } 65 | 66 | logger.log(`Researching company: ${domain}`); 67 | logger.log("Sending company research request to Exa API"); 68 | 69 | const response = await axiosInstance.post( 70 | API_CONFIG.ENDPOINTS.SEARCH, 71 | searchRequest, 72 | { timeout: 25000 } 73 | ); 74 | 75 | logger.log("Received company research response from Exa API"); 76 | 77 | if (!response.data || !response.data.results) { 78 | logger.log("Warning: Empty or invalid response from Exa API for company research"); 79 | return { 80 | content: [{ 81 | type: "text" as const, 82 | text: "No company information found. Please try a different query." 83 | }] 84 | }; 85 | } 86 | 87 | logger.log(`Found ${response.data.results.length} results about the company`); 88 | 89 | const result = { 90 | content: [{ 91 | type: "text" as const, 92 | text: JSON.stringify(response.data, null, 2) 93 | }] 94 | }; 95 | 96 | logger.complete(); 97 | return result; 98 | } catch (error) { 99 | logger.error(error); 100 | 101 | if (axios.isAxiosError(error)) { 102 | // Handle Axios errors specifically 103 | const statusCode = error.response?.status || 'unknown'; 104 | const errorMessage = error.response?.data?.message || error.message; 105 | 106 | logger.log(`Axios error (${statusCode}): ${errorMessage}`); 107 | return { 108 | content: [{ 109 | type: "text" as const, 110 | text: `Company research error (${statusCode}): ${errorMessage}` 111 | }], 112 | isError: true, 113 | }; 114 | } 115 | 116 | // Handle generic errors 117 | return { 118 | content: [{ 119 | type: "text" as const, 120 | text: `Company research error: ${error instanceof Error ? error.message : String(error)}` 121 | }], 122 | isError: true, 123 | }; 124 | } 125 | }, 126 | enabled: false // Disabled by default 127 | }; -------------------------------------------------------------------------------- /src/tools/competitorFinder.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import axios from "axios"; 3 | import { toolRegistry, API_CONFIG } from "./config.js"; 4 | import { ExaSearchRequest, ExaSearchResponse } from "../types.js"; 5 | import { createRequestLogger } from "../utils/logger.js"; 6 | 7 | // Register the competitor finder tool 8 | toolRegistry["competitor_finder"] = { 9 | name: "competitor_finder", 10 | description: "Find competitors of a company using Exa AI - performs targeted searches to identify businesses that offer similar products or services. Describe what the company does (without mentioning its name) and optionally provide the company's website to exclude it from results.", 11 | schema: { 12 | query: z.string().describe("Describe what the company/product in a few words (e.g., 'web search API', 'AI image generation', 'cloud storage service'). Keep it simple. Do not include the company name."), 13 | excludeDomain: z.string().optional().describe("Optional: The company's website to exclude from results (e.g., 'exa.ai')"), 14 | numResults: z.number().optional().describe("Number of competitors to return (default: 10)") 15 | }, 16 | handler: async ({ query, excludeDomain, numResults }, extra) => { 17 | const requestId = `competitor_finder-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; 18 | const logger = createRequestLogger(requestId, 'competitor_finder'); 19 | 20 | logger.start(query); 21 | 22 | try { 23 | // Create a fresh axios instance for each request 24 | const axiosInstance = axios.create({ 25 | baseURL: API_CONFIG.BASE_URL, 26 | headers: { 27 | 'accept': 'application/json', 28 | 'content-type': 'application/json', 29 | 'x-api-key': process.env.EXA_API_KEY || '' 30 | }, 31 | timeout: 25000 32 | }); 33 | 34 | const searchRequest: ExaSearchRequest = { 35 | query, 36 | type: "auto", 37 | numResults: numResults || 10, 38 | contents: { 39 | text: { 40 | maxCharacters: API_CONFIG.DEFAULT_MAX_CHARACTERS 41 | }, 42 | livecrawl: 'always' 43 | } 44 | }; 45 | 46 | // Add exclude domain if provided 47 | if (excludeDomain) { 48 | searchRequest.excludeDomains = [excludeDomain]; 49 | logger.log(`Excluding domain: ${excludeDomain}`); 50 | } 51 | 52 | logger.log(`Finding competitors for: ${query}`); 53 | logger.log("Sending competitor finder request to Exa API"); 54 | 55 | const response = await axiosInstance.post( 56 | API_CONFIG.ENDPOINTS.SEARCH, 57 | searchRequest, 58 | { timeout: 25000 } 59 | ); 60 | 61 | logger.log("Received competitor finder response from Exa API"); 62 | 63 | if (!response.data || !response.data.results) { 64 | logger.log("Warning: Empty or invalid response from Exa API for competitor finder"); 65 | return { 66 | content: [{ 67 | type: "text" as const, 68 | text: "No competitors found. Please try a different query." 69 | }] 70 | }; 71 | } 72 | 73 | logger.log(`Found ${response.data.results.length} potential competitors`); 74 | 75 | const result = { 76 | content: [{ 77 | type: "text" as const, 78 | text: JSON.stringify(response.data, null, 2) 79 | }] 80 | }; 81 | 82 | logger.complete(); 83 | return result; 84 | } catch (error) { 85 | logger.error(error); 86 | 87 | if (axios.isAxiosError(error)) { 88 | // Handle Axios errors specifically 89 | const statusCode = error.response?.status || 'unknown'; 90 | const errorMessage = error.response?.data?.message || error.message; 91 | 92 | logger.log(`Axios error (${statusCode}): ${errorMessage}`); 93 | return { 94 | content: [{ 95 | type: "text" as const, 96 | text: `Competitor finder error (${statusCode}): ${errorMessage}` 97 | }], 98 | isError: true, 99 | }; 100 | } 101 | 102 | // Handle generic errors 103 | return { 104 | content: [{ 105 | type: "text" as const, 106 | text: `Competitor finder error: ${error instanceof Error ? error.message : String(error)}` 107 | }], 108 | isError: true, 109 | }; 110 | } 111 | }, 112 | enabled: false // Disabled by default 113 | }; -------------------------------------------------------------------------------- /src/tools/config.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | 4 | export interface ToolRegistry { 5 | name: string; // Unique name for the tool 6 | description: string; // Human-readable description 7 | schema: z.ZodRawShape; // Zod schema for tool parameters 8 | handler: ( 9 | args: { [key: string]: any }, 10 | extra: any 11 | ) => Promise<{ 12 | content: { 13 | type: "text"; 14 | text: string; 15 | }[]; 16 | isError?: boolean; 17 | }>; // Function to execute when tool is called 18 | enabled: boolean; // Whether the tool is enabled by default 19 | } 20 | 21 | // Configuration for API 22 | export const API_CONFIG = { 23 | BASE_URL: 'https://api.exa.ai', 24 | ENDPOINTS: { 25 | SEARCH: '/search' 26 | }, 27 | DEFAULT_NUM_RESULTS: 5, 28 | DEFAULT_MAX_CHARACTERS: 3000 29 | } as const; 30 | 31 | // Tool registry that will be populated by tool modules 32 | export const toolRegistry: Record = {}; -------------------------------------------------------------------------------- /src/tools/crawling.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import axios from "axios"; 3 | import { toolRegistry, API_CONFIG } from "./config.js"; 4 | import { ExaCrawlRequest } from "../types.js"; 5 | import { createRequestLogger } from "../utils/logger.js"; 6 | 7 | // Register the crawling tool 8 | toolRegistry["crawling"] = { 9 | name: "crawling", 10 | description: "Extract content from specific URLs using Exa AI - performs targeted crawling of web pages to retrieve their full content. Useful for reading articles, PDFs, or any web page when you have the exact URL. Returns the complete text content of the specified URL.", 11 | schema: { 12 | url: z.string().describe("The URL to crawl (e.g., 'exa.ai')") 13 | }, 14 | handler: async ({ url }, extra) => { 15 | const requestId = `crawling-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; 16 | const logger = createRequestLogger(requestId, 'crawling'); 17 | 18 | logger.start(url); 19 | 20 | try { 21 | // Create a fresh axios instance for each request 22 | const axiosInstance = axios.create({ 23 | baseURL: API_CONFIG.BASE_URL, 24 | headers: { 25 | 'accept': 'application/json', 26 | 'content-type': 'application/json', 27 | 'x-api-key': process.env.EXA_API_KEY || '' 28 | }, 29 | timeout: 25000 30 | }); 31 | 32 | const crawlRequest: ExaCrawlRequest = { 33 | ids: [url], 34 | text: true, 35 | livecrawl: 'always' 36 | }; 37 | 38 | logger.log(`Crawling URL: ${url}`); 39 | logger.log("Sending crawling request to Exa API"); 40 | 41 | const response = await axiosInstance.post( 42 | '/contents', 43 | crawlRequest, 44 | { timeout: 25000 } 45 | ); 46 | 47 | logger.log("Received crawling response from Exa API"); 48 | 49 | if (!response.data || !response.data.results || response.data.results.length === 0) { 50 | logger.log("Warning: Empty or invalid response from Exa API for crawling"); 51 | return { 52 | content: [{ 53 | type: "text" as const, 54 | text: "No content found at the specified URL. Please check the URL and try again." 55 | }] 56 | }; 57 | } 58 | 59 | logger.log(`Successfully crawled content from URL`); 60 | 61 | const result = { 62 | content: [{ 63 | type: "text" as const, 64 | text: JSON.stringify(response.data, null, 2) 65 | }] 66 | }; 67 | 68 | logger.complete(); 69 | return result; 70 | } catch (error) { 71 | logger.error(error); 72 | 73 | if (axios.isAxiosError(error)) { 74 | // Handle Axios errors specifically 75 | const statusCode = error.response?.status || 'unknown'; 76 | const errorMessage = error.response?.data?.message || error.message; 77 | 78 | logger.log(`Axios error (${statusCode}): ${errorMessage}`); 79 | return { 80 | content: [{ 81 | type: "text" as const, 82 | text: `Crawling error (${statusCode}): ${errorMessage}` 83 | }], 84 | isError: true, 85 | }; 86 | } 87 | 88 | // Handle generic errors 89 | return { 90 | content: [{ 91 | type: "text" as const, 92 | text: `Crawling error: ${error instanceof Error ? error.message : String(error)}` 93 | }], 94 | isError: true, 95 | }; 96 | } 97 | }, 98 | enabled: false // Disabled by default 99 | }; -------------------------------------------------------------------------------- /src/tools/githubSearch.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import axios from "axios"; 3 | import { toolRegistry, API_CONFIG } from "./config.js"; 4 | import { ExaSearchRequest, ExaSearchResponse } from "../types.js"; 5 | import { createRequestLogger } from "../utils/logger.js"; 6 | 7 | // Register the GitHub search tool 8 | toolRegistry["github_search"] = { 9 | name: "github_search", 10 | description: "Search GitHub repositories using Exa AI - performs real-time searches on GitHub.com to find relevant repositories and GitHub accounts.", 11 | schema: { 12 | query: z.string().describe("Search query for GitHub repositories, or Github account, or code"), 13 | numResults: z.number().optional().describe("Number of search results to return (default: 5)") 14 | }, 15 | handler: async ({ query, numResults }, extra) => { 16 | const requestId = `github_search-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; 17 | const logger = createRequestLogger(requestId, 'github_search'); 18 | 19 | logger.start(query); 20 | 21 | try { 22 | // Create a fresh axios instance for each request 23 | const axiosInstance = axios.create({ 24 | baseURL: API_CONFIG.BASE_URL, 25 | headers: { 26 | 'accept': 'application/json', 27 | 'content-type': 'application/json', 28 | 'x-api-key': process.env.EXA_API_KEY || '' 29 | }, 30 | timeout: 25000 31 | }); 32 | 33 | // Prefix the query with "exa.ai GitHub:" to focus on GitHub results 34 | const githubQuery = query.toLowerCase().includes('github') ? query : `exa.ai GitHub: ${query}`; 35 | 36 | const searchRequest: ExaSearchRequest = { 37 | query: githubQuery, 38 | type: "auto", 39 | includeDomains: ["github.com"], 40 | numResults: numResults || API_CONFIG.DEFAULT_NUM_RESULTS, 41 | contents: { 42 | text: true, 43 | livecrawl: 'always' 44 | } 45 | }; 46 | 47 | logger.log("Sending request to Exa API for GitHub search"); 48 | 49 | const response = await axiosInstance.post( 50 | API_CONFIG.ENDPOINTS.SEARCH, 51 | searchRequest, 52 | { timeout: 25000 } 53 | ); 54 | 55 | logger.log("Received response from Exa API"); 56 | 57 | if (!response.data || !response.data.results) { 58 | logger.log("Warning: Empty or invalid response from Exa API"); 59 | return { 60 | content: [{ 61 | type: "text" as const, 62 | text: "No GitHub results found. Please try a different query." 63 | }] 64 | }; 65 | } 66 | 67 | logger.log(`Found ${response.data.results.length} GitHub results`); 68 | 69 | const result = { 70 | content: [{ 71 | type: "text" as const, 72 | text: JSON.stringify(response.data, null, 2) 73 | }] 74 | }; 75 | 76 | logger.complete(); 77 | return result; 78 | } catch (error) { 79 | logger.error(error); 80 | 81 | if (axios.isAxiosError(error)) { 82 | // Handle Axios errors specifically 83 | const statusCode = error.response?.status || 'unknown'; 84 | const errorMessage = error.response?.data?.message || error.message; 85 | 86 | logger.log(`Axios error (${statusCode}): ${errorMessage}`); 87 | return { 88 | content: [{ 89 | type: "text" as const, 90 | text: `GitHub search error (${statusCode}): ${errorMessage}` 91 | }], 92 | isError: true, 93 | }; 94 | } 95 | 96 | // Handle generic errors 97 | return { 98 | content: [{ 99 | type: "text" as const, 100 | text: `GitHub search error: ${error instanceof Error ? error.message : String(error)}` 101 | }], 102 | isError: true, 103 | }; 104 | } 105 | }, 106 | enabled: false 107 | }; -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | // Export the tool registry 2 | export { toolRegistry, API_CONFIG } from "./config.js"; 3 | 4 | // Import all tools to register them 5 | import "./webSearch.js"; 6 | import "./researchPaperSearch.js"; 7 | import "./companyResearch.js"; 8 | import "./crawling.js"; 9 | import "./competitorFinder.js"; 10 | import "./linkedInSearch.js"; 11 | import "./wikipediaSearch.js"; 12 | import "./githubSearch.js"; 13 | 14 | // When adding a new tool, import it here 15 | // import "./newTool.js"; -------------------------------------------------------------------------------- /src/tools/linkedInSearch.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import axios from "axios"; 3 | import { toolRegistry, API_CONFIG } from "./config.js"; 4 | import { ExaSearchRequest, ExaSearchResponse } from "../types.js"; 5 | import { createRequestLogger } from "../utils/logger.js"; 6 | 7 | // Register the LinkedIn search tool 8 | toolRegistry["linkedin_search"] = { 9 | name: "linkedin_search", 10 | description: "Search LinkedIn for companies using Exa AI. Simply include company URL, or company name, with 'company page' appended in your query.", 11 | schema: { 12 | query: z.string().describe("Search query for LinkedIn (e.g., company page OR company page)"), 13 | numResults: z.number().optional().describe("Number of search results to return (default: 5)") 14 | }, 15 | handler: async ({ query, numResults }, extra) => { 16 | const requestId = `linkedin_search-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; 17 | const logger = createRequestLogger(requestId, 'linkedin_search'); 18 | 19 | logger.start(query); 20 | 21 | try { 22 | // Create a fresh axios instance for each request 23 | const axiosInstance = axios.create({ 24 | baseURL: API_CONFIG.BASE_URL, 25 | headers: { 26 | 'accept': 'application/json', 27 | 'content-type': 'application/json', 28 | 'x-api-key': process.env.EXA_API_KEY || '' 29 | }, 30 | timeout: 25000 31 | }); 32 | 33 | // Create search request 34 | const searchRequest: ExaSearchRequest = { 35 | query, 36 | type: "auto", 37 | includeDomains: ["linkedin.com"], 38 | numResults: numResults || API_CONFIG.DEFAULT_NUM_RESULTS, 39 | contents: { 40 | text: { 41 | maxCharacters: API_CONFIG.DEFAULT_MAX_CHARACTERS 42 | }, 43 | livecrawl: 'always' 44 | } 45 | }; 46 | 47 | logger.log("Sending request to Exa API"); 48 | 49 | const response = await axiosInstance.post( 50 | API_CONFIG.ENDPOINTS.SEARCH, 51 | searchRequest, 52 | { timeout: 25000 } 53 | ); 54 | 55 | logger.log("Received response from Exa API"); 56 | 57 | if (!response.data || !response.data.results) { 58 | logger.log("Warning: Empty or invalid response from Exa API"); 59 | return { 60 | content: [{ 61 | type: "text" as const, 62 | text: "No LinkedIn results found. Please try a different query." 63 | }] 64 | }; 65 | } 66 | 67 | logger.log(`Found ${response.data.results.length} LinkedIn results`); 68 | 69 | const result = { 70 | content: [{ 71 | type: "text" as const, 72 | text: JSON.stringify(response.data, null, 2) 73 | }] 74 | }; 75 | 76 | logger.complete(); 77 | return result; 78 | } catch (error) { 79 | logger.error(error); 80 | 81 | if (axios.isAxiosError(error)) { 82 | // Handle Axios errors specifically 83 | const statusCode = error.response?.status || 'unknown'; 84 | const errorMessage = error.response?.data?.message || error.message; 85 | 86 | logger.log(`Axios error (${statusCode}): ${errorMessage}`); 87 | return { 88 | content: [{ 89 | type: "text" as const, 90 | text: `LinkedIn search error (${statusCode}): ${errorMessage}` 91 | }], 92 | isError: true, 93 | }; 94 | } 95 | 96 | // Handle generic errors 97 | return { 98 | content: [{ 99 | type: "text" as const, 100 | text: `LinkedIn search error: ${error instanceof Error ? error.message : String(error)}` 101 | }], 102 | isError: true, 103 | }; 104 | } 105 | }, 106 | enabled: false 107 | }; -------------------------------------------------------------------------------- /src/tools/researchPaperSearch.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import axios from "axios"; 3 | import { toolRegistry, API_CONFIG } from "./config.js"; 4 | import { ExaSearchRequest, ExaSearchResponse } from "../types.js"; 5 | import { createRequestLogger } from "../utils/logger.js"; 6 | 7 | // Register the research paper search tool 8 | toolRegistry["research_paper_search"] = { 9 | name: "research_paper_search", 10 | description: "Search across 100M+ research papers with full text access using Exa AI - performs targeted academic paper searches with deep research content coverage. Returns detailed information about relevant academic papers including titles, authors, publication dates, and full text excerpts. Control the number of results and character counts returned to balance comprehensiveness with conciseness based on your task requirements.", 11 | schema: { 12 | query: z.string().describe("Research topic or keyword to search for"), 13 | numResults: z.number().optional().describe("Number of research papers to return (default: 5)"), 14 | maxCharacters: z.number().optional().describe("Maximum number of characters to return for each result's text content (Default: 3000)") 15 | }, 16 | handler: async ({ query, numResults, maxCharacters }, extra) => { 17 | const requestId = `research_paper-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; 18 | const logger = createRequestLogger(requestId, 'research_paper_search'); 19 | 20 | logger.start(query); 21 | 22 | try { 23 | // Create a fresh axios instance for each request 24 | const axiosInstance = axios.create({ 25 | baseURL: API_CONFIG.BASE_URL, 26 | headers: { 27 | 'accept': 'application/json', 28 | 'content-type': 'application/json', 29 | 'x-api-key': process.env.EXA_API_KEY || '' 30 | }, 31 | timeout: 25000 32 | }); 33 | 34 | const searchRequest: ExaSearchRequest = { 35 | query, 36 | category: "research paper", 37 | type: "auto", 38 | numResults: numResults || API_CONFIG.DEFAULT_NUM_RESULTS, 39 | contents: { 40 | text: { 41 | maxCharacters: maxCharacters || API_CONFIG.DEFAULT_MAX_CHARACTERS 42 | }, 43 | livecrawl: 'fallback' 44 | } 45 | }; 46 | 47 | logger.log("Sending research paper request to Exa API"); 48 | 49 | const response = await axiosInstance.post( 50 | API_CONFIG.ENDPOINTS.SEARCH, 51 | searchRequest, 52 | { timeout: 25000 } 53 | ); 54 | 55 | logger.log("Received research paper response from Exa API"); 56 | 57 | if (!response.data || !response.data.results) { 58 | logger.log("Warning: Empty or invalid response from Exa API for research papers"); 59 | return { 60 | content: [{ 61 | type: "text" as const, 62 | text: "No research papers found. Please try a different query." 63 | }] 64 | }; 65 | } 66 | 67 | logger.log(`Found ${response.data.results.length} research papers`); 68 | 69 | const result = { 70 | content: [{ 71 | type: "text" as const, 72 | text: JSON.stringify(response.data, null, 2) 73 | }] 74 | }; 75 | 76 | logger.complete(); 77 | return result; 78 | } catch (error) { 79 | logger.error(error); 80 | 81 | if (axios.isAxiosError(error)) { 82 | // Handle Axios errors specifically 83 | const statusCode = error.response?.status || 'unknown'; 84 | const errorMessage = error.response?.data?.message || error.message; 85 | 86 | logger.log(`Axios error (${statusCode}): ${errorMessage}`); 87 | return { 88 | content: [{ 89 | type: "text" as const, 90 | text: `Research paper search error (${statusCode}): ${errorMessage}` 91 | }], 92 | isError: true, 93 | }; 94 | } 95 | 96 | // Handle generic errors 97 | return { 98 | content: [{ 99 | type: "text" as const, 100 | text: `Research paper search error: ${error instanceof Error ? error.message : String(error)}` 101 | }], 102 | isError: true, 103 | }; 104 | } 105 | }, 106 | enabled: false // disabled by default 107 | }; -------------------------------------------------------------------------------- /src/tools/webSearch.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import axios from "axios"; 3 | import { toolRegistry, API_CONFIG } from "./config.js"; 4 | import { ExaSearchRequest, ExaSearchResponse } from "../types.js"; 5 | import { createRequestLogger } from "../utils/logger.js"; 6 | 7 | // Register the web search tool 8 | toolRegistry["web_search_exa"] = { 9 | name: "web_search_exa", 10 | description: "Search the web using Exa AI - performs real-time web searches and can scrape content from specific URLs. Supports configurable result counts and returns the content from the most relevant websites.", 11 | schema: { 12 | query: z.string().describe("Search query"), 13 | numResults: z.number().optional().describe("Number of search results to return (default: 5)") 14 | }, 15 | handler: async ({ query, numResults }, extra) => { 16 | const requestId = `web_search_exa-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; 17 | const logger = createRequestLogger(requestId, 'web_search_exa'); 18 | 19 | logger.start(query); 20 | 21 | try { 22 | // Create a fresh axios instance for each request 23 | const axiosInstance = axios.create({ 24 | baseURL: API_CONFIG.BASE_URL, 25 | headers: { 26 | 'accept': 'application/json', 27 | 'content-type': 'application/json', 28 | 'x-api-key': process.env.EXA_API_KEY || '' 29 | }, 30 | timeout: 25000 31 | }); 32 | 33 | const searchRequest: ExaSearchRequest = { 34 | query, 35 | type: "auto", 36 | numResults: numResults || API_CONFIG.DEFAULT_NUM_RESULTS, 37 | contents: { 38 | text: { 39 | maxCharacters: API_CONFIG.DEFAULT_MAX_CHARACTERS 40 | }, 41 | livecrawl: 'always' 42 | } 43 | }; 44 | 45 | logger.log("Sending request to Exa API"); 46 | 47 | const response = await axiosInstance.post( 48 | API_CONFIG.ENDPOINTS.SEARCH, 49 | searchRequest, 50 | { timeout: 25000 } 51 | ); 52 | 53 | logger.log("Received response from Exa API"); 54 | 55 | if (!response.data || !response.data.results) { 56 | logger.log("Warning: Empty or invalid response from Exa API"); 57 | return { 58 | content: [{ 59 | type: "text" as const, 60 | text: "No search results found. Please try a different query." 61 | }] 62 | }; 63 | } 64 | 65 | logger.log(`Found ${response.data.results.length} results`); 66 | 67 | const result = { 68 | content: [{ 69 | type: "text" as const, 70 | text: JSON.stringify(response.data, null, 2) 71 | }] 72 | }; 73 | 74 | logger.complete(); 75 | return result; 76 | } catch (error) { 77 | logger.error(error); 78 | 79 | if (axios.isAxiosError(error)) { 80 | // Handle Axios errors specifically 81 | const statusCode = error.response?.status || 'unknown'; 82 | const errorMessage = error.response?.data?.message || error.message; 83 | 84 | logger.log(`Axios error (${statusCode}): ${errorMessage}`); 85 | return { 86 | content: [{ 87 | type: "text" as const, 88 | text: `Search error (${statusCode}): ${errorMessage}` 89 | }], 90 | isError: true, 91 | }; 92 | } 93 | 94 | // Handle generic errors 95 | return { 96 | content: [{ 97 | type: "text" as const, 98 | text: `Search error: ${error instanceof Error ? error.message : String(error)}` 99 | }], 100 | isError: true, 101 | }; 102 | } 103 | }, 104 | enabled: true // Enabled by default 105 | }; -------------------------------------------------------------------------------- /src/tools/wikipediaSearch.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import axios from "axios"; 3 | import { toolRegistry, API_CONFIG } from "./config.js"; 4 | import { ExaSearchRequest, ExaSearchResponse } from "../types.js"; 5 | import { createRequestLogger } from "../utils/logger.js"; 6 | 7 | // Register the Wikipedia search tool 8 | toolRegistry["wikipedia_search_exa"] = { 9 | name: "wikipedia_search_exa", 10 | description: "Search Wikipedia using Exa AI - performs searches specifically within Wikipedia.org and returns relevant content from Wikipedia pages.", 11 | schema: { 12 | query: z.string().describe("Search query for Wikipedia"), 13 | numResults: z.number().optional().describe("Number of search results to return (default: 5)") 14 | }, 15 | handler: async ({ query, numResults }, extra) => { 16 | const requestId = `wikipedia_search_exa-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`; 17 | const logger = createRequestLogger(requestId, 'wikipedia_search_exa'); 18 | 19 | logger.start(query); 20 | 21 | try { 22 | // Create a fresh axios instance for each request 23 | const axiosInstance = axios.create({ 24 | baseURL: API_CONFIG.BASE_URL, 25 | headers: { 26 | 'accept': 'application/json', 27 | 'content-type': 'application/json', 28 | 'x-api-key': process.env.EXA_API_KEY || '' 29 | }, 30 | timeout: 25000 31 | }); 32 | 33 | const searchRequest: ExaSearchRequest = { 34 | query, 35 | type: "auto", 36 | includeDomains: ["wikipedia.org"], 37 | numResults: numResults || API_CONFIG.DEFAULT_NUM_RESULTS, 38 | contents: { 39 | text: { 40 | maxCharacters: API_CONFIG.DEFAULT_MAX_CHARACTERS 41 | }, 42 | livecrawl: 'always' 43 | } 44 | }; 45 | 46 | logger.log("Sending request to Exa API for Wikipedia search"); 47 | 48 | const response = await axiosInstance.post( 49 | API_CONFIG.ENDPOINTS.SEARCH, 50 | searchRequest, 51 | { timeout: 25000 } 52 | ); 53 | 54 | logger.log("Received response from Exa API"); 55 | 56 | if (!response.data || !response.data.results) { 57 | logger.log("Warning: Empty or invalid response from Exa API"); 58 | return { 59 | content: [{ 60 | type: "text" as const, 61 | text: "No Wikipedia search results found. Please try a different query." 62 | }] 63 | }; 64 | } 65 | 66 | logger.log(`Found ${response.data.results.length} Wikipedia results`); 67 | 68 | const result = { 69 | content: [{ 70 | type: "text" as const, 71 | text: JSON.stringify(response.data, null, 2) 72 | }] 73 | }; 74 | 75 | logger.complete(); 76 | return result; 77 | } catch (error) { 78 | logger.error(error); 79 | 80 | if (axios.isAxiosError(error)) { 81 | // Handle Axios errors specifically 82 | const statusCode = error.response?.status || 'unknown'; 83 | const errorMessage = error.response?.data?.message || error.message; 84 | 85 | logger.log(`Axios error (${statusCode}): ${errorMessage}`); 86 | return { 87 | content: [{ 88 | type: "text" as const, 89 | text: `Wikipedia search error (${statusCode}): ${errorMessage}` 90 | }], 91 | isError: true, 92 | }; 93 | } 94 | 95 | // Handle generic errors 96 | return { 97 | content: [{ 98 | type: "text" as const, 99 | text: `Wikipedia search error: ${error instanceof Error ? error.message : String(error)}` 100 | }], 101 | isError: true, 102 | }; 103 | } 104 | }, 105 | enabled: false 106 | }; -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // Exa API Types 2 | export interface ExaSearchRequest { 3 | query: string; 4 | type: string; 5 | category?: string; 6 | includeDomains?: string[]; 7 | excludeDomains?: string[]; 8 | startPublishedDate?: string; 9 | endPublishedDate?: string; 10 | numResults: number; 11 | contents: { 12 | text: { 13 | maxCharacters?: number; 14 | } | boolean; 15 | livecrawl?: 'always' | 'fallback'; 16 | subpages?: number; 17 | subpageTarget?: string[]; 18 | }; 19 | } 20 | 21 | export interface ExaCrawlRequest { 22 | ids: string[]; 23 | text: boolean; 24 | livecrawl?: 'always' | 'fallback'; 25 | } 26 | 27 | export interface ExaSearchResult { 28 | id: string; 29 | title: string; 30 | url: string; 31 | publishedDate: string; 32 | author: string; 33 | text: string; 34 | image?: string; 35 | favicon?: string; 36 | score?: number; 37 | } 38 | 39 | export interface ExaSearchResponse { 40 | requestId: string; 41 | autopromptString: string; 42 | resolvedSearchType: string; 43 | results: ExaSearchResult[]; 44 | } 45 | 46 | // Tool Types 47 | export interface SearchArgs { 48 | query: string; 49 | numResults?: number; 50 | livecrawl?: 'always' | 'fallback'; 51 | } -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple logging utility for MCP server 3 | */ 4 | export const log = (message: string): void => { 5 | console.error(`[EXA-MCP-DEBUG] ${message}`); 6 | }; 7 | 8 | export const createRequestLogger = (requestId: string, toolName: string) => { 9 | return { 10 | log: (message: string): void => { 11 | log(`[${requestId}] [${toolName}] ${message}`); 12 | }, 13 | start: (query: string): void => { 14 | log(`[${requestId}] [${toolName}] Starting search for query: "${query}"`); 15 | }, 16 | error: (error: unknown): void => { 17 | log(`[${requestId}] [${toolName}] Error: ${error instanceof Error ? error.message : String(error)}`); 18 | }, 19 | complete: (): void => { 20 | log(`[${requestId}] [${toolName}] Successfully completed request`); 21 | } 22 | }; 23 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "declaration": true, 13 | "declarationMap": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | --------------------------------------------------------------------------------