#
tokens: 19140/50000 21/22 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 2. Use http://codebase.md/exa-labs/exa-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── .npmignore
├── Dockerfile
├── LICENSE
├── llm_mcp_docs.txt
├── mcp_publishing_steps_on_mcpregistry.md
├── package-lock.json
├── package.json
├── README.md
├── server.json
├── smithery-example.json
├── smithery.yaml
├── src
│   ├── index.ts
│   ├── tools
│   │   ├── companyResearch.ts
│   │   ├── config.ts
│   │   ├── crawling.ts
│   │   ├── deepResearchCheck.ts
│   │   ├── deepResearchStart.ts
│   │   ├── exaCode.ts
│   │   ├── linkedInSearch.ts
│   │   └── webSearch.ts
│   ├── types.ts
│   └── utils
│       └── logger.ts
└── tsconfig.json
```

# Files

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

```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
5 | .smithery/
```

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
1 | src/
2 | tests/
3 | .github/
4 | .gitignore
5 | .npmignore
6 | tsconfig.json
7 | *.log
8 | .env* 
```

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

```markdown
  1 | # Exa MCP Server 🔍
  2 | [![npm version](https://badge.fury.io/js/exa-mcp-server.svg)](https://www.npmjs.com/package/exa-mcp-server)
  3 | [![smithery badge](https://smithery.ai/badge/exa)](https://smithery.ai/server/exa)
  4 | 
  5 | ## 🆕 `exa-code`: fast, efficient web context for coding agents
  6 | 
  7 | Vibe coding should never have a bad vibe. `exa-code` is a huge step towards coding agents that never hallucinate.
  8 | 
  9 | When your coding agent makes a search query, `exa-code` searches over billions
 10 | of Github repos, docs pages, Stackoverflow posts, and more, to find the perfect, token-efficient context that the agent needs to code correctly. It's powered by the Exa search engine.
 11 | 
 12 | Examples of queries you can make with `exa-code`:
 13 | * use Exa search in python and make sure content is always livecrawled
 14 | * use correct syntax for vercel ai sdk to call gpt-5 nano asking it how are you
 15 | * how to set up a reproducible Nix Rust development environment
 16 | 
 17 | **✨ Works with Cursor and Claude Code!** Use the HTTP-based configuration format:
 18 | 
 19 | ```json
 20 | {
 21 |   "mcpServers": {
 22 |     "exa": {
 23 |       "type": "http",
 24 |       "url": "https://mcp.exa.ai/mcp",
 25 |       "headers": {
 26 |         "Remove-Me": "Disable web_search_exa tool if you're just coding. To 100% call exa-code, say 'use exa-code'."
 27 |       }
 28 |     }
 29 |   }
 30 | }
 31 | ```
 32 | 
 33 | You may include your exa api key in the url like this:
 34 | ```
 35 | https://mcp.exa.ai/mcp?exaApiKey=YOUREXAKEY
 36 | ```
 37 | 
 38 | You may whitelist specific tools in the url with the `enabledTools` parameter which expects a url encoded array strings like this:
 39 | ```
 40 | https://mcp.exa.ai/mcp?exaApiKey=YOUREXAKEY&enabledTools=%5B%22crawling_exa%ss%5D
 41 | ```
 42 | 
 43 | You can also use `exa-code` through [Smithery](https://smithery.ai/server/exa) without an Exa API key.
 44 | 
 45 | ---
 46 | 
 47 | A Model Context Protocol (MCP) server that connects AI assistants like Claude to Exa AI's search capabilities, including web search, research tools, and our new code search feature.
 48 | 
 49 | ## Remote Exa MCP 🌐
 50 | 
 51 | Connect directly to Exa's hosted MCP server (instead of running it locally).
 52 | 
 53 | ### Remote Exa MCP URL
 54 | 
 55 | ```
 56 | https://mcp.exa.ai/mcp
 57 | ```
 58 | 
 59 | ### Claude Desktop Configuration for Remote MCP
 60 | 
 61 | Add this to your Claude Desktop configuration file:
 62 | 
 63 | ```json
 64 | {
 65 |   "mcpServers": {
 66 |     "exa": {
 67 |       "command": "npx",
 68 |       "args": [
 69 |         "-y",
 70 |         "mcp-remote",
 71 |         "https://mcp.exa.ai/mcp"
 72 |       ]
 73 |     }
 74 |   }
 75 | }
 76 | ```
 77 | 
 78 | ### Cursor and Claude Code Configuration for Remote MCP
 79 | 
 80 | For Cursor and Claude Code, use this HTTP-based configuration format:
 81 | 
 82 | ```json
 83 | {
 84 |   "mcpServers": {
 85 |     "exa": {
 86 |       "type": "http",
 87 |       "url": "https://mcp.exa.ai/mcp",
 88 |       "headers": {}
 89 |     }
 90 |   }
 91 | }
 92 | ```
 93 | 
 94 | ### NPM Installation
 95 | 
 96 | ```bash
 97 | npm install -g exa-mcp-server
 98 | ```
 99 | 
100 | ### Using Claude Code
101 | 
102 | ```bash
103 | claude mcp add exa -e EXA_API_KEY=YOUR_API_KEY -- npx -y exa-mcp-server
104 | ```
105 | 
106 | ### Using Exa MCP through Smithery
107 | 
108 | To install the Exa MCP server via [Smithery](https://smithery.ai/server/exa), head over to:
109 | 
110 | [smithery.ai/server/exa](https://smithery.ai/server/exa)
111 | 
112 | 
113 | ## Configuration ⚙️
114 | 
115 | ### 1. Configure Claude Desktop to recognize the Exa MCP server
116 | 
117 | You can find claude_desktop_config.json inside the settings of Claude Desktop app:
118 | 
119 | Open the Claude Desktop app and enable Developer Mode from the top-left menu bar. 
120 | 
121 | 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. 
122 | 
123 | OR (if you want to open claude_desktop_config.json from terminal)
124 | 
125 | #### For macOS:
126 | 
127 | 1. Open your Claude Desktop configuration:
128 | 
129 | ```bash
130 | code ~/Library/Application\ Support/Claude/claude_desktop_config.json
131 | ```
132 | 
133 | #### For Windows:
134 | 
135 | 1. Open your Claude Desktop configuration:
136 | 
137 | ```powershell
138 | code %APPDATA%\Claude\claude_desktop_config.json
139 | ```
140 | 
141 | ### 2. Add the Exa server configuration:
142 | 
143 | ```json
144 | {
145 |   "mcpServers": {
146 |     "exa": {
147 |       "command": "npx",
148 |       "args": ["-y", "exa-mcp-server"],
149 |       "env": {
150 |         "EXA_API_KEY": "your-api-key-here"
151 |       }
152 |     }
153 |   }
154 | }
155 | ```
156 | 
157 | Replace `your-api-key-here` with your actual Exa API key from [dashboard.exa.ai/api-keys](https://dashboard.exa.ai/api-keys).
158 | 
159 | ### 3. Available Tools & Tool Selection
160 | 
161 | The Exa MCP server includes powerful tools for developers and researchers:
162 | 
163 | #### 🔥 **Featured: Code Search Tool**
164 | - **get_code_context_exa**: 🆕 **NEW!** Search and get relevant code snippets, examples, and documentation from open source libraries, GitHub repositories, and programming frameworks. Perfect for finding up-to-date code documentation, implementation examples, API usage patterns, and best practices from real codebases.
165 | 
166 | #### 🌐 **Other Available Tools**
167 | - **web_search_exa**: Performs real-time web searches with optimized results and content extraction.
168 | - **company_research**: Comprehensive company research tool that crawls company websites to gather detailed information about businesses.
169 | - **crawling**: Extracts content from specific URLs, useful for reading articles, PDFs, or any web page when you have the exact URL.
170 | - **linkedin_search**: Search LinkedIn for companies and people using Exa AI. Simply include company names, person names, or specific LinkedIn URLs in your query.
171 | - **deep_researcher_start**: Start a smart AI researcher for complex questions. The AI will search the web, read many sources, and think deeply about your question to create a detailed research report.
172 | - **deep_researcher_check**: Check if your research is ready and get the results. Use this after starting a research task to see if it's done and get your comprehensive report.
173 | 
174 | You can choose which tools to enable by adding the `--tools` parameter to your Claude Desktop configuration:
175 | 
176 | #### 💻 **Setup for Code Search Only** (Recommended for Developers)
177 | 
178 | ```json
179 | {
180 |   "mcpServers": {
181 |     "exa": {
182 |       "command": "npx",
183 |       "args": [
184 |         "-y",
185 |         "exa-mcp-server",
186 |         "--tools=get_code_context_exa,web_search_exa"
187 |       ],
188 |       "env": {
189 |         "EXA_API_KEY": "your-api-key-here"
190 |       }
191 |     }
192 |   }
193 | }
194 | ```
195 | 
196 | #### Specify which tools to enable:
197 | 
198 | ```json
199 | {
200 |   "mcpServers": {
201 |     "exa": {
202 |       "command": "npx",
203 |       "args": [
204 |         "-y",
205 |         "exa-mcp-server",
206 |         "--tools=get_code_context_exa,web_search_exa,company_research,crawling,linkedin_search,deep_researcher_start,deep_researcher_check"
207 |       ],
208 |       "env": {
209 |         "EXA_API_KEY": "your-api-key-here"
210 |       }
211 |     }
212 |   }
213 | }
214 | ```
215 | 
216 | For enabling multiple tools, use a comma-separated list:
217 | 
218 | ```json
219 | {
220 |   "mcpServers": {
221 |     "exa": {
222 |       "command": "npx",
223 |       "args": [
224 |         "-y",
225 |         "exa-mcp-server",
226 |         "--tools=get_code_context_exa,web_search_exa,company_research,crawling,linkedin_search,deep_researcher_start,deep_researcher_check"
227 |       ],
228 |       "env": {
229 |         "EXA_API_KEY": "your-api-key-here"
230 |       }
231 |     }
232 |   }
233 | }
234 | ```
235 | 
236 | If you don't specify any tools, all tools enabled by default will be used.
237 | 
238 | ### 4. Restart Claude Desktop
239 | 
240 | For the changes to take effect:
241 | 
242 | 1. Completely quit Claude Desktop (not just close the window)
243 | 2. Start Claude Desktop again
244 | 3. Look for the icon to verify the Exa server is connected
245 | 
246 | ## Using via NPX
247 | 
248 | If you prefer to run the server directly, you can use npx:
249 | 
250 | ```bash
251 | # Run with all tools enabled by default
252 | npx exa-mcp-server
253 | 
254 | # Enable specific tools only
255 | npx exa-mcp-server --tools=web_search_exa
256 | 
257 | # Enable multiple tools
258 | npx exa-mcp-server --tools=web_search_exa,get_code_context_exa
259 | 
260 | # List all available tools
261 | npx exa-mcp-server --list-tools
262 | ```
263 | 
264 | ---
265 | 
266 | Built with ❤️ by team Exa
267 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
1 | runtime: typescript 
```

--------------------------------------------------------------------------------
/smithery-example.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "exaApiKey": "your-exa-api-key-here",
 3 |   "enabledTools": [
 4 |     "web_search_exa",
 5 |     "company_research_exa",
 6 |     "crawling_exa",
 7 |     "linkedin_search_exa",
 8 |     "deep_researcher_start",
 9 |     "deep_researcher_check"
10 |   ],
11 |   "debug": false
12 | } 
```

--------------------------------------------------------------------------------
/src/tools/config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // Configuration for API
 2 | export const API_CONFIG = {
 3 |   BASE_URL: 'https://api.exa.ai',
 4 |   ENDPOINTS: {
 5 |     SEARCH: '/search',
 6 |     RESEARCH_TASKS: '/research/v0/tasks',
 7 |     CONTEXT: '/context'
 8 |   },
 9 |   DEFAULT_NUM_RESULTS: 8,
10 |   DEFAULT_MAX_CHARACTERS: 2000
11 | } as const; 
```

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

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

--------------------------------------------------------------------------------
/server.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
 3 |   "name": "io.github.exa-labs/exa-mcp-server",
 4 |   "description": "MCP server with Exa for web search and web crawling. Exa is the search engine for AI Applications.",
 5 |   "version": "3.0.5",
 6 |   "packages": [
 7 |     {
 8 |       "registryType": "npm",
 9 |       "identifier": "exa-mcp-server",
10 |       "version": "3.0.5"
11 |     }
12 |   ],
13 |   "remotes": [
14 |     {
15 |       "type": "sse",
16 |       "url": "https://mcp.exa.ai/mcp?exaApiKey=your-exa-api-key",
17 |       "description": "Hosted Exa MCP server with web search and web crawling capabilities. Get the API key from https://dashboard.exa.ai/api-keys"
18 |     }
19 |   ]
20 | }
21 | 
```

--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------

```typescript
 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 | }; 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```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 for Docker
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/.smithery ./.smithery
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", ".smithery/index.cjs"]
```

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

```json
 1 | {
 2 |   "name": "exa-mcp-server",
 3 |   "version": "3.0.5",
 4 |   "description": "A Model Context Protocol server with Exa for web search and web crawling. 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 |   "mcpName": "io.github.exa-labs/exa-mcp-server",
 6 |   "type": "module",
 7 |   "module": "./src/index.ts",
 8 |   "repository": {
 9 |     "type": "git",
10 |     "url": "git+https://github.com/exa-labs/exa-mcp-server.git"
11 |   },
12 |   "bin": {
13 |     "exa-mcp-server": ".smithery/stdio/index.cjs"
14 |   },
15 |   "files": [
16 |     ".smithery"
17 |   ],
18 |   "keywords": [
19 |     "mcp",
20 |     "search mcp",
21 |     "model context protocol",
22 |     "exa",
23 |     "search",
24 |     "websearch",
25 |     "claude",
26 |     "ai",
27 |     "research",
28 |     "papers",
29 |     "linkedin"
30 |   ],
31 |   "author": "Exa Labs",
32 |   "scripts": {
33 |     "build": "npm run build:shttp && npm run build:stdio",
34 |     "build:stdio": "smithery build src/index.ts --transport stdio -o .smithery/stdio/index.cjs && echo '#!/usr/bin/env node' | cat - .smithery/stdio/index.cjs > temp && mv temp .smithery/stdio/index.cjs && chmod +x .smithery/stdio/index.cjs",
35 |     "build:shttp": "smithery build src/index.ts --transport shttp -o .smithery/shttp/index.cjs",
36 |     "prepare": "npm run build:stdio",
37 |     "watch": "tsc --watch",
38 |     "dev": "npx @smithery/cli@latest dev",
39 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js",
40 |     "prepublishOnly": "npm run build:stdio"
41 |   },
42 |   "dependencies": {
43 |     "@modelcontextprotocol/sdk": "^1.12.1",
44 |     "axios": "^1.7.8",
45 |     "zod": "^3.22.4"
46 |   },
47 |   "devDependencies": {
48 |     "@smithery/cli": "^1.4.4",
49 |     "@types/node": "^20.11.24",
50 |     "tsx": "^4.7.0",
51 |     "typescript": "^5.3.3"
52 |   },
53 |   "engines": {
54 |     "node": ">=18.0.0"
55 |   }
56 | }
```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
  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' | 'preferred';
 16 |     subpages?: number;
 17 |     subpageTarget?: string[];
 18 |   };
 19 | }
 20 | 
 21 | export interface ExaCrawlRequest {
 22 |   ids: string[];
 23 |   text: boolean;
 24 |   livecrawl?: 'always' | 'fallback' | 'preferred';
 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' | 'preferred';
 51 | }
 52 | 
 53 | // Deep Research API Types
 54 | export interface DeepResearchRequest {
 55 |   model: 'exa-research' | 'exa-research-pro';
 56 |   instructions: string;
 57 |   output?: {
 58 |     inferSchema?: boolean;
 59 |   };
 60 | }
 61 | 
 62 | export interface DeepResearchStartResponse {
 63 |   id: string;
 64 |   outputSchema?: {
 65 |     type: string;
 66 |     properties: any;
 67 |     required: string[];
 68 |     additionalProperties: boolean;
 69 |   };
 70 | }
 71 | 
 72 | export interface DeepResearchCheckResponse {
 73 |   id: string;
 74 |   createdAt: number;
 75 |   status: 'running' | 'completed' | 'failed';
 76 |   instructions: string;
 77 |   schema?: {
 78 |     type: string;
 79 |     properties: any;
 80 |     required: string[];
 81 |     additionalProperties: boolean;
 82 |   };
 83 |   data?: {
 84 |     report?: string;
 85 |     [key: string]: any;
 86 |   };
 87 |   operations?: Array<{
 88 |     type: string;
 89 |     stepId: string;
 90 |     text?: string;
 91 |     query?: string;
 92 |     goal?: string;
 93 |     results?: any[];
 94 |     url?: string;
 95 |     thought?: string;
 96 |     data?: any;
 97 |   }>;
 98 |   citations?: {
 99 |     [key: string]: Array<{
100 |       id: string;
101 |       url: string;
102 |       title: string;
103 |       snippet: string;
104 |     }>;
105 |   };
106 |   timeMs?: number;
107 |   model?: string;
108 |   costDollars?: {
109 |     total: number;
110 |     research: {
111 |       searches: number;
112 |       pages: number;
113 |       reasoningTokens: number;
114 |     };
115 |   };
116 | }
117 | 
118 | export interface DeepResearchErrorResponse {
119 |   response: {
120 |     message: string;
121 |     error: string;
122 |     statusCode: number;
123 |   };
124 |   status: number;
125 |   options: any;
126 |   message: string;
127 |   name: string;
128 | }
129 | 
130 | // Exa Code API Types
131 | export interface ExaCodeRequest {
132 |   query: string;
133 |   tokensNum: "dynamic" | number;
134 |   flags?: string[];
135 | }
136 | 
137 | export interface ExaCodeResult {
138 |   id: string;
139 |   title: string;
140 |   url: string;
141 |   text: string;
142 |   score?: number;
143 | }
144 | 
145 | export interface ExaCodeResponse {
146 |   requestId: string;
147 |   query: string;
148 |   repository?: string;
149 |   response: string;
150 |   resultsCount: number;
151 |   costDollars: string;
152 |   searchTime: number;
153 |   outputTokens?: number;
154 |   traces?: any;
155 | }
```

--------------------------------------------------------------------------------
/src/tools/crawling.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import axios from "axios";
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { API_CONFIG } from "./config.js";
  5 | import { createRequestLogger } from "../utils/logger.js";
  6 | 
  7 | export function registerCrawlingTool(server: McpServer, config?: { exaApiKey?: string }): void {
  8 |   server.tool(
  9 |     "crawling_exa",
 10 |     "Extract and crawl content from specific URLs using Exa AI - retrieves full text content, metadata, and structured information from web pages. Ideal for extracting detailed content from known URLs.",
 11 |     {
 12 |       url: z.string().describe("URL to crawl and extract content from"),
 13 |       maxCharacters: z.number().optional().describe("Maximum characters to extract (default: 3000)")
 14 |     },
 15 |     async ({ url, maxCharacters }) => {
 16 |       const requestId = `crawling_exa-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
 17 |       const logger = createRequestLogger(requestId, 'crawling_exa');
 18 |       
 19 |       logger.start(url);
 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': config?.exaApiKey || process.env.EXA_API_KEY || ''
 29 |           },
 30 |           timeout: 25000
 31 |         });
 32 | 
 33 |         const crawlRequest = {
 34 |           ids: [url],
 35 |           contents: {
 36 |             text: {
 37 |               maxCharacters: maxCharacters || API_CONFIG.DEFAULT_MAX_CHARACTERS
 38 |             },
 39 |             livecrawl: 'preferred'
 40 |           }
 41 |         };
 42 |         
 43 |         logger.log("Sending crawl request to Exa API");
 44 |         
 45 |         const response = await axiosInstance.post(
 46 |           '/contents',
 47 |           crawlRequest,
 48 |           { timeout: 25000 }
 49 |         );
 50 |         
 51 |         logger.log("Received response from Exa API");
 52 | 
 53 |         if (!response.data || !response.data.results) {
 54 |           logger.log("Warning: Empty or invalid response from Exa API");
 55 |           return {
 56 |             content: [{
 57 |               type: "text" as const,
 58 |               text: "No content found for the provided URL."
 59 |             }]
 60 |           };
 61 |         }
 62 | 
 63 |         logger.log(`Successfully crawled content from URL`);
 64 |         
 65 |         const result = {
 66 |           content: [{
 67 |             type: "text" as const,
 68 |             text: JSON.stringify(response.data, null, 2)
 69 |           }]
 70 |         };
 71 |         
 72 |         logger.complete();
 73 |         return result;
 74 |       } catch (error) {
 75 |         logger.error(error);
 76 |         
 77 |         if (axios.isAxiosError(error)) {
 78 |           // Handle Axios errors specifically
 79 |           const statusCode = error.response?.status || 'unknown';
 80 |           const errorMessage = error.response?.data?.message || error.message;
 81 |           
 82 |           logger.log(`Axios error (${statusCode}): ${errorMessage}`);
 83 |           return {
 84 |             content: [{
 85 |               type: "text" as const,
 86 |               text: `Crawling error (${statusCode}): ${errorMessage}`
 87 |             }],
 88 |             isError: true,
 89 |           };
 90 |         }
 91 |         
 92 |         // Handle generic errors
 93 |         return {
 94 |           content: [{
 95 |             type: "text" as const,
 96 |             text: `Crawling error: ${error instanceof Error ? error.message : String(error)}`
 97 |           }],
 98 |           isError: true,
 99 |         };
100 |       }
101 |     }
102 |   );
103 | } 
```

--------------------------------------------------------------------------------
/src/tools/webSearch.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import axios from "axios";
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { API_CONFIG } from "./config.js";
  5 | import { ExaSearchRequest, ExaSearchResponse } from "../types.js";
  6 | import { createRequestLogger } from "../utils/logger.js";
  7 | 
  8 | export function registerWebSearchTool(server: McpServer, config?: { exaApiKey?: string }): void {
  9 |   server.tool(
 10 |     "web_search_exa",
 11 |     "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.",
 12 |     {
 13 |       query: z.string().describe("Search query"),
 14 |       numResults: z.number().optional().describe("Number of search results to return (default: 5)")
 15 |     },
 16 |     async ({ query, numResults }) => {
 17 |       const requestId = `web_search_exa-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
 18 |       const logger = createRequestLogger(requestId, 'web_search_exa');
 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': config?.exaApiKey || process.env.EXA_API_KEY || ''
 30 |           },
 31 |           timeout: 25000
 32 |         });
 33 | 
 34 |         const searchRequest: ExaSearchRequest = {
 35 |           query,
 36 |           type: "auto",
 37 |           numResults: numResults || API_CONFIG.DEFAULT_NUM_RESULTS,
 38 |           contents: {
 39 |             text: {
 40 |               maxCharacters: API_CONFIG.DEFAULT_MAX_CHARACTERS
 41 |             },
 42 |             livecrawl: 'preferred'
 43 |           }
 44 |         };
 45 |         
 46 |         logger.log("Sending request to Exa API");
 47 |         
 48 |         const response = await axiosInstance.post<ExaSearchResponse>(
 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 search results found. Please try a different query."
 62 |             }]
 63 |           };
 64 |         }
 65 | 
 66 |         logger.log(`Found ${response.data.results.length} 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: `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: `Search error: ${error instanceof Error ? error.message : String(error)}`
100 |           }],
101 |           isError: true,
102 |         };
103 |       }
104 |     }
105 |   );
106 | } 
```

--------------------------------------------------------------------------------
/src/tools/companyResearch.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import axios from "axios";
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { API_CONFIG } from "./config.js";
  5 | import { ExaSearchRequest, ExaSearchResponse } from "../types.js";
  6 | import { createRequestLogger } from "../utils/logger.js";
  7 | 
  8 | export function registerCompanyResearchTool(server: McpServer, config?: { exaApiKey?: string }): void {
  9 |   server.tool(
 10 |     "company_research_exa",
 11 |     "Research companies using Exa AI - finds comprehensive information about businesses, organizations, and corporations. Provides insights into company operations, news, financial information, and industry analysis.",
 12 |     {
 13 |       companyName: z.string().describe("Name of the company to research"),
 14 |       numResults: z.number().optional().describe("Number of search results to return (default: 5)")
 15 |     },
 16 |     async ({ companyName, numResults }) => {
 17 |       const requestId = `company_research_exa-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
 18 |       const logger = createRequestLogger(requestId, 'company_research_exa');
 19 |       
 20 |       logger.start(companyName);
 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': config?.exaApiKey || process.env.EXA_API_KEY || ''
 30 |           },
 31 |           timeout: 25000
 32 |         });
 33 | 
 34 |         const searchRequest: ExaSearchRequest = {
 35 |           query: `${companyName} company business corporation information news financial`,
 36 |           type: "auto",
 37 |           numResults: numResults || API_CONFIG.DEFAULT_NUM_RESULTS,
 38 |           contents: {
 39 |             text: {
 40 |               maxCharacters: API_CONFIG.DEFAULT_MAX_CHARACTERS
 41 |             },
 42 |             livecrawl: 'preferred'
 43 |           },
 44 |           includeDomains: ["bloomberg.com", "reuters.com", "crunchbase.com", "sec.gov", "linkedin.com", "forbes.com", "businesswire.com", "prnewswire.com"]
 45 |         };
 46 |         
 47 |         logger.log("Sending request to Exa API for company research");
 48 |         
 49 |         const response = await axiosInstance.post<ExaSearchResponse>(
 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 company information found. Please try a different company name."
 63 |             }]
 64 |           };
 65 |         }
 66 | 
 67 |         logger.log(`Found ${response.data.results.length} company research 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: `Company research 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: `Company research error: ${error instanceof Error ? error.message : String(error)}`
101 |           }],
102 |           isError: true,
103 |         };
104 |       }
105 |     }
106 |   );
107 | } 
```

--------------------------------------------------------------------------------
/src/tools/linkedInSearch.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import axios from "axios";
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { API_CONFIG } from "./config.js";
  5 | import { ExaSearchRequest, ExaSearchResponse } from "../types.js";
  6 | import { createRequestLogger } from "../utils/logger.js";
  7 | 
  8 | export function registerLinkedInSearchTool(server: McpServer, config?: { exaApiKey?: string }): void {
  9 |   server.tool(
 10 |     "linkedin_search_exa",
 11 |     "Search LinkedIn profiles and companies using Exa AI - finds professional profiles, company pages, and business-related content on LinkedIn. Useful for networking, recruitment, and business research.",
 12 |     {
 13 |       query: z.string().describe("LinkedIn search query (e.g., person name, company, job title)"),
 14 |       searchType: z.enum(["profiles", "companies", "all"]).optional().describe("Type of LinkedIn content to search (default: all)"),
 15 |       numResults: z.number().optional().describe("Number of LinkedIn results to return (default: 5)")
 16 |     },
 17 |     async ({ query, searchType, numResults }) => {
 18 |       const requestId = `linkedin_search_exa-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
 19 |       const logger = createRequestLogger(requestId, 'linkedin_search_exa');
 20 |       
 21 |       logger.start(`${query} (${searchType || 'all'})`);
 22 |       
 23 |       try {
 24 |         // Create a fresh axios instance for each request
 25 |         const axiosInstance = axios.create({
 26 |           baseURL: API_CONFIG.BASE_URL,
 27 |           headers: {
 28 |             'accept': 'application/json',
 29 |             'content-type': 'application/json',
 30 |             'x-api-key': config?.exaApiKey || process.env.EXA_API_KEY || ''
 31 |           },
 32 |           timeout: 25000
 33 |         });
 34 | 
 35 |         let searchQuery = query;
 36 |         if (searchType === "profiles") {
 37 |           searchQuery = `${query} LinkedIn profile`;
 38 |         } else if (searchType === "companies") {
 39 |           searchQuery = `${query} LinkedIn company`;
 40 |         } else {
 41 |           searchQuery = `${query} LinkedIn`;
 42 |         }
 43 | 
 44 |         const searchRequest: ExaSearchRequest = {
 45 |           query: searchQuery,
 46 |           type: "neural",
 47 |           numResults: numResults || API_CONFIG.DEFAULT_NUM_RESULTS,
 48 |           contents: {
 49 |             text: {
 50 |               maxCharacters: API_CONFIG.DEFAULT_MAX_CHARACTERS
 51 |             },
 52 |             livecrawl: 'preferred'
 53 |           },
 54 |           includeDomains: ["linkedin.com"]
 55 |         };
 56 |         
 57 |         logger.log("Sending request to Exa API for LinkedIn search");
 58 |         
 59 |         const response = await axiosInstance.post<ExaSearchResponse>(
 60 |           API_CONFIG.ENDPOINTS.SEARCH,
 61 |           searchRequest,
 62 |           { timeout: 25000 }
 63 |         );
 64 |         
 65 |         logger.log("Received response from Exa API");
 66 | 
 67 |         if (!response.data || !response.data.results) {
 68 |           logger.log("Warning: Empty or invalid response from Exa API");
 69 |           return {
 70 |             content: [{
 71 |               type: "text" as const,
 72 |               text: "No LinkedIn content found. Please try a different query."
 73 |             }]
 74 |           };
 75 |         }
 76 | 
 77 |         logger.log(`Found ${response.data.results.length} LinkedIn results`);
 78 |         
 79 |         const result = {
 80 |           content: [{
 81 |             type: "text" as const,
 82 |             text: JSON.stringify(response.data, null, 2)
 83 |           }]
 84 |         };
 85 |         
 86 |         logger.complete();
 87 |         return result;
 88 |       } catch (error) {
 89 |         logger.error(error);
 90 |         
 91 |         if (axios.isAxiosError(error)) {
 92 |           // Handle Axios errors specifically
 93 |           const statusCode = error.response?.status || 'unknown';
 94 |           const errorMessage = error.response?.data?.message || error.message;
 95 |           
 96 |           logger.log(`Axios error (${statusCode}): ${errorMessage}`);
 97 |           return {
 98 |             content: [{
 99 |               type: "text" as const,
100 |               text: `LinkedIn search error (${statusCode}): ${errorMessage}`
101 |             }],
102 |             isError: true,
103 |           };
104 |         }
105 |         
106 |         // Handle generic errors
107 |         return {
108 |           content: [{
109 |             type: "text" as const,
110 |             text: `LinkedIn search error: ${error instanceof Error ? error.message : String(error)}`
111 |           }],
112 |           isError: true,
113 |         };
114 |       }
115 |     }
116 |   );
117 | } 
```

--------------------------------------------------------------------------------
/src/tools/exaCode.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import axios from "axios";
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { API_CONFIG } from "./config.js";
  5 | import { ExaCodeRequest, ExaCodeResponse } from "../types.js";
  6 | import { createRequestLogger } from "../utils/logger.js";
  7 | 
  8 | export function registerExaCodeTool(server: McpServer, config?: { exaApiKey?: string }): void {
  9 |   server.tool(
 10 |     "get_code_context_exa",
 11 |     "Search and get relevant context for any programming task. Exa-code has the highest quality and freshest context for libraries, SDKs, and APIs. Use this tool for ANY question or task for related to programming. RULE: when the user's query contains exa-code or anything related to code, you MUST use this tool.",
 12 |     {
 13 |       query: z.string().describe("Search query to find relevant context for APIs, Libraries, and SDKs. For example, 'React useState hook examples', 'Python pandas dataframe filtering', 'Express.js middleware', 'Next js partial prerendering configuration'"),
 14 |       tokensNum: z.union([z.literal("dynamic"), z.number().min(1000).max(50000)]).default("dynamic").describe("Token allocation strategy: 'dynamic' (default, token-efficient, returns the 100-1000+ most useful tokens), 1000-50000 tokens (returns a specific number of tokens). Use 'dynamic' for optimal token efficiency - only specify a concrete number of tokens if 'dynamic' mode doesn't return the right information.")
 15 |     },
 16 |     async ({ query, tokensNum }) => {
 17 |       const requestId = `get_code_context_exa-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
 18 |       const logger = createRequestLogger(requestId, 'get_code_context_exa');
 19 |       
 20 |       logger.start(`Searching for code context: ${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': config?.exaApiKey || process.env.EXA_API_KEY || ''
 30 |           },
 31 |           timeout: 30000
 32 |         });
 33 | 
 34 |         const exaCodeRequest: ExaCodeRequest = {
 35 |           query,
 36 |           tokensNum
 37 |         };
 38 |         
 39 |         logger.log("Sending code context request to Exa API");
 40 |         
 41 |         const response = await axiosInstance.post<ExaCodeResponse>(
 42 |           API_CONFIG.ENDPOINTS.CONTEXT,
 43 |           exaCodeRequest,
 44 |           { timeout: 30000 }
 45 |         );
 46 |         
 47 |         logger.log("Received code context response from Exa API");
 48 | 
 49 |         if (!response.data) {
 50 |           logger.log("Warning: Empty response from Exa Code API");
 51 |           return {
 52 |             content: [{
 53 |               type: "text" as const,
 54 |               text: "No code snippets or documentation found. Please try a different query, be more specific about the library or programming concept, or check the spelling of framework names."
 55 |             }]
 56 |           };
 57 |         }
 58 | 
 59 |         logger.log(`Code search completed with ${response.data.resultsCount || 0} results`);
 60 |         
 61 |         // Return the actual code content from the response field
 62 |         const codeContent = typeof response.data.response === 'string' 
 63 |           ? response.data.response 
 64 |           : JSON.stringify(response.data.response, null, 2);
 65 |         
 66 |         const result = {
 67 |           content: [{
 68 |             type: "text" as const,
 69 |             text: codeContent
 70 |           }]
 71 |         };
 72 |         
 73 |         logger.complete();
 74 |         return result;
 75 |       } catch (error) {
 76 |         logger.error(error);
 77 |         
 78 |         if (axios.isAxiosError(error)) {
 79 |           // Handle Axios errors specifically
 80 |           const statusCode = error.response?.status || 'unknown';
 81 |           const errorMessage = error.response?.data?.message || error.message;
 82 |           
 83 |           logger.log(`Axios error (${statusCode}): ${errorMessage}`);
 84 |           return {
 85 |             content: [{
 86 |               type: "text" as const,
 87 |               text: `Code search error (${statusCode}): ${errorMessage}. Please check your query and try again.`
 88 |             }],
 89 |             isError: true,
 90 |           };
 91 |         }
 92 |         
 93 |         // Handle generic errors
 94 |         return {
 95 |           content: [{
 96 |             type: "text" as const,
 97 |             text: `Code search error: ${error instanceof Error ? error.message : String(error)}`
 98 |           }],
 99 |           isError: true,
100 |         };
101 |       }
102 |     }
103 |   );
104 | }
105 | 
```

--------------------------------------------------------------------------------
/src/tools/deepResearchStart.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import axios from "axios";
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { API_CONFIG } from "./config.js";
  5 | import { DeepResearchRequest, DeepResearchStartResponse } from "../types.js";
  6 | import { createRequestLogger } from "../utils/logger.js";
  7 | 
  8 | export function registerDeepResearchStartTool(server: McpServer, config?: { exaApiKey?: string }): void {
  9 |   server.tool(
 10 |     "deep_researcher_start",
 11 |     "Start a comprehensive AI-powered deep research task for complex queries. This tool initiates an intelligent agent that performs extensive web searches, crawls relevant pages, analyzes information, and synthesizes findings into a detailed research report. The agent thinks critically about the research topic and provides thorough, well-sourced answers. Use this for complex research questions that require in-depth analysis rather than simple searches. After starting a research task, IMMEDIATELY use deep_researcher_check with the returned task ID to monitor progress and retrieve results.",
 12 |     {
 13 |       instructions: z.string().describe("Complex research question or detailed instructions for the AI researcher. Be specific about what you want to research and any particular aspects you want covered."),
 14 |       model: z.enum(['exa-research', 'exa-research-pro']).optional().describe("Research model: 'exa-research' (faster, 15-45s, good for most queries) or 'exa-research-pro' (more comprehensive, 45s-2min, for complex topics). Default: exa-research")
 15 |     },
 16 |     async ({ instructions, model }) => {
 17 |       const requestId = `deep_researcher_start-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
 18 |       const logger = createRequestLogger(requestId, 'deep_researcher_start');
 19 |       
 20 |       logger.start(instructions);
 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': config?.exaApiKey || process.env.EXA_API_KEY || ''
 30 |           },
 31 |           timeout: 25000
 32 |         });
 33 | 
 34 |         const researchRequest: DeepResearchRequest = {
 35 |           model: model || 'exa-research',
 36 |           instructions,
 37 |           output: {
 38 |             inferSchema: false
 39 |           }
 40 |         };
 41 |         
 42 |         logger.log(`Starting research with model: ${researchRequest.model}`);
 43 |         
 44 |         const response = await axiosInstance.post<DeepResearchStartResponse>(
 45 |           API_CONFIG.ENDPOINTS.RESEARCH_TASKS,
 46 |           researchRequest,
 47 |           { timeout: 25000 }
 48 |         );
 49 |         
 50 |         logger.log(`Research task started with ID: ${response.data.id}`);
 51 | 
 52 |         if (!response.data || !response.data.id) {
 53 |           logger.log("Warning: Empty or invalid response from Exa Research API");
 54 |           return {
 55 |             content: [{
 56 |               type: "text" as const,
 57 |               text: "Failed to start research task. Please try again."
 58 |             }],
 59 |             isError: true,
 60 |           };
 61 |         }
 62 | 
 63 |         const result = {
 64 |           content: [{
 65 |             type: "text" as const,
 66 |             text: JSON.stringify({
 67 |               success: true,
 68 |               taskId: response.data.id,
 69 |               model: researchRequest.model,
 70 |               instructions: instructions,
 71 |               outputSchema: response.data.outputSchema,
 72 |               message: `Deep research task started successfully with ${researchRequest.model} model. IMMEDIATELY use deep_researcher_check with task ID '${response.data.id}' to monitor progress. Keep checking every few seconds until status is 'completed' to get the research results.`,
 73 |               nextStep: `Call deep_researcher_check with taskId: "${response.data.id}"`
 74 |             }, null, 2)
 75 |           }]
 76 |         };
 77 |         
 78 |         logger.complete();
 79 |         return result;
 80 |       } catch (error) {
 81 |         logger.error(error);
 82 |         
 83 |         if (axios.isAxiosError(error)) {
 84 |           // Handle Axios errors specifically
 85 |           const statusCode = error.response?.status || 'unknown';
 86 |           const errorMessage = error.response?.data?.message || error.message;
 87 |           
 88 |           logger.log(`Axios error (${statusCode}): ${errorMessage}`);
 89 |           return {
 90 |             content: [{
 91 |               type: "text" as const,
 92 |               text: `Research start error (${statusCode}): ${errorMessage}`
 93 |             }],
 94 |             isError: true,
 95 |           };
 96 |         }
 97 |         
 98 |         // Handle generic errors
 99 |         return {
100 |           content: [{
101 |             type: "text" as const,
102 |             text: `Research start error: ${error instanceof Error ? error.message : String(error)}`
103 |           }],
104 |           isError: true,
105 |         };
106 |       }
107 |     }
108 |   );
109 | } 
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  3 | import { z } from "zod";
  4 | 
  5 | // Import tool implementations
  6 | import { registerWebSearchTool } from "./tools/webSearch.js";
  7 | import { registerCompanyResearchTool } from "./tools/companyResearch.js";
  8 | import { registerCrawlingTool } from "./tools/crawling.js";
  9 | import { registerLinkedInSearchTool } from "./tools/linkedInSearch.js";
 10 | import { registerDeepResearchStartTool } from "./tools/deepResearchStart.js";
 11 | import { registerDeepResearchCheckTool } from "./tools/deepResearchCheck.js";
 12 | import { registerExaCodeTool } from "./tools/exaCode.js";
 13 | import { log } from "./utils/logger.js";
 14 | 
 15 | // Configuration schema for the EXA API key and tool selection
 16 | export const configSchema = z.object({
 17 |   exaApiKey: z.string().optional().describe("Exa AI API key for search operations"),
 18 |   enabledTools: z.array(z.string()).optional().describe("List of tools to enable (if not specified, all tools are enabled)"),
 19 |   debug: z.boolean().default(false).describe("Enable debug logging")
 20 | });
 21 | 
 22 | // Export stateless flag for MCP
 23 | export const stateless = true;
 24 | 
 25 | // Tool registry for managing available tools
 26 | const availableTools = {
 27 |   'web_search_exa': { name: 'Web Search (Exa)', description: 'Real-time web search using Exa AI', enabled: true },
 28 |   'get_code_context_exa': { name: 'Code Context Search', description: 'Search for code snippets, examples, and documentation from open source repositories', enabled: true },
 29 |   'crawling_exa': { name: 'Web Crawling', description: 'Extract content from specific URLs', enabled: false },
 30 |   'deep_researcher_start': { name: 'Deep Researcher Start', description: 'Start a comprehensive AI research task', enabled: false },
 31 |   'deep_researcher_check': { name: 'Deep Researcher Check', description: 'Check status and retrieve results of research task', enabled: false },
 32 |   'linkedin_search_exa': { name: 'LinkedIn Search', description: 'Search LinkedIn profiles and companies', enabled: false },
 33 |   'company_research_exa': { name: 'Company Research', description: 'Research companies and organizations', enabled: false },
 34 | };  
 35 | 
 36 | /**
 37 |  * Exa AI Web Search MCP Server
 38 |  * 
 39 |  * This MCP server integrates Exa AI's search capabilities with Claude and other MCP-compatible clients.
 40 |  * Exa is a search engine and API specifically designed for up-to-date web searching and retrieval,
 41 |  * offering more recent and comprehensive results than what might be available in an LLM's training data.
 42 |  * 
 43 |  * The server provides tools that enable:
 44 |  * - Real-time web searching with configurable parameters
 45 |  * - Company research and analysis
 46 |  * - Web content crawling
 47 |  * - LinkedIn search capabilities
 48 |  * - Deep research workflows
 49 |  * - And more!
 50 |  */
 51 | 
 52 | export default function ({ config }: { config: z.infer<typeof configSchema> }) {
 53 |   try {
 54 |     // Set the API key in environment for tool functions to use
 55 |     // process.env.EXA_API_KEY = config.exaApiKey;
 56 |     
 57 |     if (config.debug) {
 58 |       log("Starting Exa MCP Server in debug mode");
 59 |     }
 60 | 
 61 |     // Create MCP server
 62 |     const server = new McpServer({
 63 |       name: "exa-search-server",
 64 |       title: "Exa",
 65 |       version: "3.0.5"
 66 |     });
 67 |     
 68 |     log("Server initialized with modern MCP SDK and Smithery CLI support");
 69 | 
 70 |     // Helper function to check if a tool should be registered
 71 |     const shouldRegisterTool = (toolId: string): boolean => {
 72 |       if (config.enabledTools && config.enabledTools.length > 0) {
 73 |         return config.enabledTools.includes(toolId);
 74 |       }
 75 |       return availableTools[toolId as keyof typeof availableTools]?.enabled ?? false;
 76 |     };
 77 | 
 78 |     // Register tools based on configuration
 79 |     const registeredTools: string[] = [];
 80 |     
 81 |     if (shouldRegisterTool('web_search_exa')) {
 82 |       registerWebSearchTool(server, config);
 83 |       registeredTools.push('web_search_exa');
 84 |     }
 85 |     
 86 |     if (shouldRegisterTool('company_research_exa')) {
 87 |       registerCompanyResearchTool(server, config);
 88 |       registeredTools.push('company_research_exa');
 89 |     }
 90 |     
 91 |     if (shouldRegisterTool('crawling_exa')) {
 92 |       registerCrawlingTool(server, config);
 93 |       registeredTools.push('crawling_exa');
 94 |     }
 95 |     
 96 |     if (shouldRegisterTool('linkedin_search_exa')) {
 97 |       registerLinkedInSearchTool(server, config);
 98 |       registeredTools.push('linkedin_search_exa');
 99 |     }
100 |     
101 |     if (shouldRegisterTool('deep_researcher_start')) {
102 |       registerDeepResearchStartTool(server, config);
103 |       registeredTools.push('deep_researcher_start');
104 |     }
105 |     
106 |     if (shouldRegisterTool('deep_researcher_check')) {
107 |       registerDeepResearchCheckTool(server, config);
108 |       registeredTools.push('deep_researcher_check');
109 |     }
110 |     
111 |     if (shouldRegisterTool('get_code_context_exa')) {
112 |       registerExaCodeTool(server, config);
113 |       registeredTools.push('get_code_context_exa');
114 |     }
115 |     
116 |     if (config.debug) {
117 |       log(`Registered ${registeredTools.length} tools: ${registeredTools.join(', ')}`);
118 |     }
119 |     
120 |     // Return the server object (Smithery CLI handles transport)
121 |     return server.server;
122 |     
123 |   } catch (error) {
124 |     log(`Server initialization error: ${error instanceof Error ? error.message : String(error)}`);
125 |     throw error;
126 |   }
127 | }
128 | 
```

--------------------------------------------------------------------------------
/src/tools/deepResearchCheck.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { z } from "zod";
  2 | import axios from "axios";
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { API_CONFIG } from "./config.js";
  5 | import { DeepResearchCheckResponse, DeepResearchErrorResponse } from "../types.js";
  6 | import { createRequestLogger } from "../utils/logger.js";
  7 | 
  8 | // Helper function to create a delay
  9 | function delay(ms: number): Promise<void> {
 10 |   return new Promise(resolve => setTimeout(resolve, ms));
 11 | }
 12 | 
 13 | export function registerDeepResearchCheckTool(server: McpServer, config?: { exaApiKey?: string }): void {
 14 |   server.tool(
 15 |     "deep_researcher_check",
 16 |     "Check the status and retrieve results of a deep research task. This tool monitors the progress of an AI agent that performs comprehensive web searches, analyzes multiple sources, and synthesizes findings into detailed research reports. The tool includes a built-in 5-second delay before checking to allow processing time. IMPORTANT: You must call this tool repeatedly (poll) until the status becomes 'completed' to get the final research results. When status is 'running', wait a few seconds and call this tool again with the same task ID.",
 17 |     {
 18 |       taskId: z.string().describe("The task ID returned from deep_researcher_start tool")
 19 |     },
 20 |     async ({ taskId }) => {
 21 |       const requestId = `deep_researcher_check-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
 22 |       const logger = createRequestLogger(requestId, 'deep_researcher_check');
 23 |       
 24 |       logger.start(taskId);
 25 |       
 26 |       try {
 27 |         // Built-in delay to allow processing time
 28 |         logger.log("Waiting 5 seconds before checking status...");
 29 |         await delay(5000);
 30 | 
 31 |         // Create a fresh axios instance for each request
 32 |         const axiosInstance = axios.create({
 33 |           baseURL: API_CONFIG.BASE_URL,
 34 |           headers: {
 35 |             'accept': 'application/json',
 36 |             'x-api-key': config?.exaApiKey || process.env.EXA_API_KEY || ''
 37 |           },
 38 |           timeout: 25000
 39 |         });
 40 | 
 41 |         logger.log(`Checking status for task: ${taskId}`);
 42 |         
 43 |         const response = await axiosInstance.get<DeepResearchCheckResponse>(
 44 |           `${API_CONFIG.ENDPOINTS.RESEARCH_TASKS}/${taskId}`,
 45 |           { timeout: 25000 }
 46 |         );
 47 |         
 48 |         logger.log(`Task status: ${response.data.status}`);
 49 | 
 50 |         if (!response.data) {
 51 |           logger.log("Warning: Empty response from Exa Research API");
 52 |           return {
 53 |             content: [{
 54 |               type: "text" as const,
 55 |               text: "Failed to check research task status. Please try again."
 56 |             }],
 57 |             isError: true,
 58 |           };
 59 |         }
 60 | 
 61 |         // Format the response based on status
 62 |         let resultText: string;
 63 |         
 64 |         if (response.data.status === 'completed') {
 65 |           // Task completed - return only the essential research report to avoid context overflow
 66 |           resultText = JSON.stringify({
 67 |             success: true,
 68 |             status: response.data.status,
 69 |             taskId: response.data.id,
 70 |             report: response.data.data?.report || "No report generated",
 71 |             timeMs: response.data.timeMs,
 72 |             model: response.data.model,
 73 |             message: "🎉 Deep research completed! Here's your comprehensive research report."
 74 |           }, null, 2);
 75 |           logger.log("Research completed successfully");
 76 |         } else if (response.data.status === 'running') {
 77 |           // Task still running - return minimal status to avoid filling context window
 78 |           resultText = JSON.stringify({
 79 |             success: true,
 80 |             status: response.data.status,
 81 |             taskId: response.data.id,
 82 |             message: "🔄 Research in progress. Continue polling...",
 83 |             nextAction: "Call deep_researcher_check again with the same task ID"
 84 |           }, null, 2);
 85 |           logger.log("Research still in progress");
 86 |         } else if (response.data.status === 'failed') {
 87 |           // Task failed
 88 |           resultText = JSON.stringify({
 89 |             success: false,
 90 |             status: response.data.status,
 91 |             taskId: response.data.id,
 92 |             createdAt: new Date(response.data.createdAt).toISOString(),
 93 |             instructions: response.data.instructions,
 94 |             message: "❌ Deep research task failed. Please try starting a new research task with different instructions."
 95 |           }, null, 2);
 96 |           logger.log("Research task failed");
 97 |         } else {
 98 |           // Unknown status
 99 |           resultText = JSON.stringify({
100 |             success: false,
101 |             status: response.data.status,
102 |             taskId: response.data.id,
103 |             message: `⚠️ Unknown status: ${response.data.status}. Continue polling or restart the research task.`
104 |           }, null, 2);
105 |           logger.log(`Unknown status: ${response.data.status}`);
106 |         }
107 | 
108 |         const result = {
109 |           content: [{
110 |             type: "text" as const,
111 |             text: resultText
112 |           }]
113 |         };
114 |         
115 |         logger.complete();
116 |         return result;
117 |       } catch (error) {
118 |         logger.error(error);
119 |         
120 |         if (axios.isAxiosError(error)) {
121 |           // Handle specific 404 error for task not found
122 |           if (error.response?.status === 404) {
123 |             const errorData = error.response.data as DeepResearchErrorResponse;
124 |             logger.log(`Task not found: ${taskId}`);
125 |             return {
126 |               content: [{
127 |                 type: "text" as const,
128 |                 text: JSON.stringify({
129 |                   success: false,
130 |                   error: "Task not found",
131 |                   taskId: taskId,
132 |                   message: "🚫 The specified task ID was not found. Please check the ID or start a new research task using deep_researcher_start."
133 |                 }, null, 2)
134 |               }],
135 |               isError: true,
136 |             };
137 |           }
138 |           
139 |           // Handle other Axios errors
140 |           const statusCode = error.response?.status || 'unknown';
141 |           const errorMessage = error.response?.data?.message || error.message;
142 |           
143 |           logger.log(`Axios error (${statusCode}): ${errorMessage}`);
144 |           return {
145 |             content: [{
146 |               type: "text" as const,
147 |               text: `Research check error (${statusCode}): ${errorMessage}`
148 |             }],
149 |             isError: true,
150 |           };
151 |         }
152 |         
153 |         // Handle generic errors
154 |         return {
155 |           content: [{
156 |             type: "text" as const,
157 |             text: `Research check error: ${error instanceof Error ? error.message : String(error)}`
158 |           }],
159 |           isError: true,
160 |         };
161 |       }
162 |     }
163 |   );
164 | } 
```

--------------------------------------------------------------------------------
/mcp_publishing_steps_on_mcpregistry.md:
--------------------------------------------------------------------------------

```markdown
  1 | # MCP Registry Publishing Setup - Exa MCP Server
  2 | 
  3 | This document outlines the setup for publishing the Exa MCP Server to the official MCP Registry with **hybrid deployment** support (both NPM package and remote server options).
  4 | 
  5 | ## Files Created/Modified
  6 | 
  7 | ### 1. `server.json` ✅
  8 | - **Purpose**: MCP Registry configuration file
  9 | - **Namespace**: `io.github.exa-labs/exa-mcp-server`
 10 | - **Deployment Type**: **Hybrid** (Package + Remote)
 11 | - **Schema**: Uses official 2025-07-09 schema
 12 | 
 13 | **Package Deployment**:
 14 | - Registry: NPM (`exa-mcp-server`)
 15 | - Version: 3.0.5
 16 | 
 17 | **Remote Deployment**:
 18 | - Type: Server-Sent Events (SSE)
 19 | - URL: `https://mcp.exa.ai/mcp`
 20 | - Authentication: API key passed as query parameter (`?exaApiKey=your-key`)
 21 | 
 22 | ### 2. `package.json` ✅
 23 | - **Added**: `mcpName` field for NPM validation
 24 | - **Value**: `"io.github.exa-labs/exa-mcp-server"`
 25 | - **Purpose**: Proves ownership of NPM package for registry validation
 26 | 
 27 | ## Deployment Options for Users
 28 | 
 29 | Your MCP server will offer users **two ways** to connect:
 30 | 
 31 | ### Option 1: NPM Package (Local Installation)
 32 | ```bash
 33 | # Install globally
 34 | npm install -g exa-mcp-server
 35 | 
 36 | # Run with tools
 37 | npx exa-mcp-server --tools=web_search_exa,deep_researcher_start
 38 | ```
 39 | 
 40 | **Claude Desktop Configuration**:
 41 | ```json
 42 | {
 43 |   "mcpServers": {
 44 |     "exa": {
 45 |       "command": "npx",
 46 |       "args": ["-y", "exa-mcp-server"],
 47 |       "env": {
 48 |         "EXA_API_KEY": "your-api-key-here"
 49 |       }
 50 |     }
 51 |   }
 52 | }
 53 | ```
 54 | 
 55 | ### Option 2: Remote Server (Hosted)
 56 | **Claude Desktop Configuration**:
 57 | ```json
 58 | {
 59 |   "mcpServers": {
 60 |     "exa": {
 61 |       "command": "npx",
 62 |       "args": [
 63 |         "-y",
 64 |         "mcp-remote",
 65 |         "https://mcp.exa.ai/mcp?exaApiKey=your-exa-api-key"
 66 |       ]
 67 |     }
 68 |   }
 69 | }
 70 | ```
 71 | 
 72 | ## Manual Publishing Process
 73 | 
 74 | ### Prerequisites
 75 | 1. **Install MCP Publisher CLI**:
 76 |    ```bash
 77 |    # macOS/Linux
 78 |    brew install mcp-publisher
 79 |    
 80 |    # Or download binary
 81 |    curl -L "https://github.com/modelcontextprotocol/registry/releases/download/v1.0.0/mcp-publisher_1.0.0_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher && sudo mv mcp-publisher /usr/local/bin/
 82 |    ```
 83 | 
 84 | 2. **Ensure NPM Package is Published**:
 85 |    - Your NPM package must be published with the `mcpName` field
 86 |    - Current package: `[email protected]`
 87 | 
 88 | 3. **Ensure Remote Server is Live**:
 89 |    - Your SSE endpoint must be accessible at: `https://mcp.exa.ai/mcp`
 90 |    - Must accept `exaApiKey` parameter for authentication
 91 | 
 92 | ### Publishing Steps
 93 | 
 94 | 1. **Authenticate with GitHub**:
 95 |    ```bash
 96 |    mcp-publisher login github
 97 |    ```
 98 |    This opens your browser for OAuth authentication.
 99 | 
100 | 2. **Validate Configuration**:
101 |    ```bash
102 |    # Optional: validate your server.json
103 |    python3 -c "
104 |    import json
105 |    with open('server.json', 'r') as f:
106 |        data = json.load(f)
107 |    print('✓ server.json is valid')
108 |    print(f'✓ Name: {data[\"name\"]}')
109 |    print(f'✓ Packages: {len(data[\"packages\"])} configured')
110 |    print(f'✓ Remotes: {len(data[\"remotes\"])} configured')
111 |    "
112 |    ```
113 | 
114 | 3. **Publish to Registry**:
115 |    ```bash
116 |    mcp-publisher publish
117 |    ```
118 | 
119 | 4. **Verify Publication**:
120 |    ```bash
121 |    curl "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.exa-labs/exa-mcp-server"
122 |    ```
123 | 
124 | ## Registry Validation Process
125 | 
126 | ### NPM Package Validation
127 | - Registry fetches: `https://registry.npmjs.org/exa-mcp-server`
128 | - Validates: `mcpName` field matches `io.github.exa-labs/exa-mcp-server`
129 | - Status: ✅ Configured correctly
130 | 
131 | ### Remote Server Validation
132 | - Registry checks: `https://mcp.exa.ai/mcp` is accessible
133 | - Validates: SSE endpoint responds correctly
134 | - Authentication: API key passed via URL query parameter (`?exaApiKey=your-key`)
135 | 
136 | ### GitHub Authentication
137 | - Namespace: `io.github.exa-labs/*`
138 | - Authentication: GitHub OAuth (no DNS setup required)
139 | - Organization: Must have access to `exa-labs` GitHub organization
140 | 
141 | ## Available Tools
142 | 
143 | When published, users will have access to these tools:
144 | 
145 | | Tool                    | Description                                                                                                                                                                 |
146 | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
147 | | `deep_researcher_start` | Start a smart AI researcher for complex questions. The AI will search the web, read many sources, and think deeply about your question to create a detailed research report |
148 | | `deep_researcher_check` | Check if your research is ready and get the results. Use this after starting a research task to see if it's done and get your comprehensive report                          |
149 | | `web_search_exa`        | Performs real-time web searches with optimized results and content extraction                                                                                               |
150 | | `company_research`      | Comprehensive company research tool that crawls company websites to gather detailed information about businesses                                                            |
151 | | `crawling`              | Extracts content from specific URLs, useful for reading articles, PDFs, or any web page when you have the exact URL                                                         |
152 | | `linkedin_search`       | Search LinkedIn for companies and people using Exa AI. Simply include company names, person names, or specific LinkedIn URLs in your query                                  |
153 | | `get_code_context_exa`  | Search and get relevant code snippets, examples, and documentation from open source libraries, GitHub repositories, and programming frameworks                             |
154 | 
155 | ## Benefits of Hybrid Deployment
156 | 
157 | 1. **User Choice**: Users can choose between local (NPM) or remote (hosted) deployment
158 | 2. **Flexibility**: Local for privacy/control, remote for convenience
159 | 3. **Scalability**: Remote server handles the load
160 | 4. **Reliability**: Multiple deployment options ensure availability
161 | 
162 | ## Troubleshooting
163 | 
164 | ### Common Issues
165 | 
166 | 1. **"Package validation failed"**
167 |    - Ensure `exa-mcp-server` NPM package has `mcpName` field
168 |    - Check package is published and accessible
169 | 
170 | 2. **"Remote validation failed"**
171 |    - Verify `https://mcp.exa.ai/mcp` is accessible
172 |    - Check SSE endpoint responds correctly
173 |    - Ensure URL accepts `?exaApiKey=your-key` query parameter
174 | 
175 | 3. **"Authentication failed"**
176 |    - Verify GitHub access to `exa-labs` organization
177 |    - Re-run `mcp-publisher login github`
178 | 
179 | 4. **"Namespace not authorized"**
180 |    - Ensure you have access to `exa-labs` GitHub organization
181 |    - Check authentication method matches namespace
182 | 
183 | ## Next Steps
184 | 
185 | 1. **Verify Prerequisites**:
186 |    - ✅ NPM package published with `mcpName` field
187 |    - ✅ Remote server live at `https://mcp.exa.ai/mcp`
188 |    - ✅ GitHub access to `exa-labs` organization
189 | 
190 | 2. **Publish to Registry**:
191 |    ```bash
192 |    mcp-publisher login github
193 |    mcp-publisher publish
194 |    ```
195 | 
196 | 3. **Verify Publication**:
197 |    - Check registry API response
198 |    - Test both deployment methods
199 |    - Update documentation as needed
200 | 
201 | ## Documentation References
202 | 
203 | - [MCP Publishing Guide](https://raw.githubusercontent.com/modelcontextprotocol/registry/refs/heads/main/docs/guides/publishing/publish-server.md)
204 | - [Server.json Schema](https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json)
205 | - [Remote Server Configuration](https://raw.githubusercontent.com/modelcontextprotocol/registry/refs/heads/main/docs/reference/server-json/generic-server-json.md#remote-server-example)
206 | - [Hybrid Deployment Examples](https://raw.githubusercontent.com/modelcontextprotocol/registry/refs/heads/main/docs/reference/server-json/generic-server-json.md#server-with-remote-and-package-options)
```
Page 1/2FirstPrevNextLast