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