├── .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 | [](https://www.npmjs.com/package/exa-mcp-server)
3 | [](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 |
--------------------------------------------------------------------------------