exa-mcp-server
Version:
A Model Context Protocol server with Exa for web search, academic paper search, and Twitter/X.com search. Provides real-time web searches with configurable tool selection, allowing users to enable or disable specific search capabilities. Supports customiz
109 lines (108 loc) • 5.08 kB
JavaScript
import { z } from "zod";
import axios from "axios";
import { toolRegistry, API_CONFIG } from "./config.js";
import { createRequestLogger } from "../utils/logger.js";
// Register the company research tool
toolRegistry["company_research"] = {
name: "company_research",
description: "Research companies using Exa AI - performs targeted searches of company websites to gather comprehensive information about businesses. Returns detailed information from company websites including about pages, pricing, FAQs, blogs, and other relevant content. Specify the company URL and optionally target specific sections of their website.",
schema: {
query: z.string().describe("Company website URL (e.g., 'exa.ai' or 'https://exa.ai')"),
subpages: z.number().optional().describe("Number of subpages to crawl (default: 10)"),
subpageTarget: z.array(z.string()).optional().describe("Specific sections to target (e.g., ['about', 'pricing', 'faq', 'blog']). If not provided, will crawl the most relevant pages.")
},
handler: async ({ query, numResults, subpages, subpageTarget }, extra) => {
const requestId = `company_research-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`;
const logger = createRequestLogger(requestId, 'company_research');
logger.start(query);
try {
// Create a fresh axios instance for each request
const axiosInstance = axios.create({
baseURL: API_CONFIG.BASE_URL,
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'x-api-key': process.env.EXA_API_KEY || ''
},
timeout: 25000
});
// Extract domain from query if it's a URL
let domain = query;
if (query.includes('http')) {
try {
const url = new URL(query);
domain = url.hostname.replace('www.', '');
}
catch (e) {
logger.log(`Warning: Could not parse URL from query: ${query}`);
}
}
const searchRequest = {
query,
category: "company",
includeDomains: [query],
type: "auto",
numResults: 1,
contents: {
text: {
maxCharacters: API_CONFIG.DEFAULT_MAX_CHARACTERS
},
livecrawl: 'always',
subpages: subpages || 10,
}
};
// Add subpage targets if provided
if (subpageTarget && subpageTarget.length > 0) {
searchRequest.contents.subpageTarget = subpageTarget;
logger.log(`Targeting specific sections: ${subpageTarget.join(', ')}`);
}
logger.log(`Researching company: ${domain}`);
logger.log("Sending company research request to Exa API");
const response = await axiosInstance.post(API_CONFIG.ENDPOINTS.SEARCH, searchRequest, { timeout: 25000 });
logger.log("Received company research response from Exa API");
if (!response.data || !response.data.results) {
logger.log("Warning: Empty or invalid response from Exa API for company research");
return {
content: [{
type: "text",
text: "No company information found. Please try a different query."
}]
};
}
logger.log(`Found ${response.data.results.length} results about the company`);
const result = {
content: [{
type: "text",
text: JSON.stringify(response.data, null, 2)
}]
};
logger.complete();
return result;
}
catch (error) {
logger.error(error);
if (axios.isAxiosError(error)) {
// Handle Axios errors specifically
const statusCode = error.response?.status || 'unknown';
const errorMessage = error.response?.data?.message || error.message;
logger.log(`Axios error (${statusCode}): ${errorMessage}`);
return {
content: [{
type: "text",
text: `Company research error (${statusCode}): ${errorMessage}`
}],
isError: true,
};
}
// Handle generic errors
return {
content: [{
type: "text",
text: `Company research error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true,
};
}
},
enabled: false // Disabled by default
};