Skip to main content
Glama

Exa MCP Server

by joerup
client.ts14 kB
import Exa from "exa-js"; import { z } from "zod"; import { ExaError } from './types.js'; /** * Client for interacting with the Exa AI API */ export class ExaClient { private readonly apiKey: string; private client: any; constructor(apiKey: string) { if (!apiKey) { throw new Error('API key is required'); } this.apiKey = apiKey; this.client = new (Exa as any)(apiKey); } /** * Perform a web search using Exa AI */ async search(params: { query: string; num_results?: number; include_domains?: string[]; exclude_domains?: string[]; start_crawl_date?: string; end_crawl_date?: string; start_published_date?: string; end_published_date?: string; use_autoprompt?: boolean; type?: 'keyword' | 'neural' | 'magic'; category?: string; include_text?: boolean; include_highlights?: boolean; include_summary?: boolean; }) { try { const searchParams: any = { query: params.query, numResults: params.num_results || 10, useAutoprompt: params.use_autoprompt ?? true, type: params.type || 'neural' }; if (params.include_domains) searchParams.includeDomains = params.include_domains; if (params.exclude_domains) searchParams.excludeDomains = params.exclude_domains; if (params.start_crawl_date) searchParams.startCrawlDate = params.start_crawl_date; if (params.end_crawl_date) searchParams.endCrawlDate = params.end_crawl_date; if (params.start_published_date) searchParams.startPublishedDate = params.start_published_date; if (params.end_published_date) searchParams.endPublishedDate = params.end_published_date; if (params.category) searchParams.category = params.category; const searchResponse = await this.client.searchAndContents( searchParams.query, { ...searchParams, text: params.include_text ? { maxCharacters: 2000, includeHtmlTags: false } : undefined, highlights: params.include_highlights ? { numSentences: 3, highlightsPerUrl: 3 } : undefined, summary: params.include_summary ? { query: params.query } : undefined } ); return searchResponse.results; } catch (error) { throw new ExaError( error instanceof Error ? error.message : 'Failed to search', 'SEARCH_ERROR' ); } } /** * Research companies and organizations */ async researchCompany(params: { company_name: string; include_news?: boolean; include_overview?: boolean; include_competitors?: boolean; include_reviews?: boolean; max_results_per_category?: number; }) { try { const results: any = {}; const maxResults = params.max_results_per_category || 5; if (params.include_overview !== false) { const overviewSearch = await this.search({ query: `"${params.company_name}" company overview about products services`, num_results: maxResults, type: 'neural', include_text: true, include_summary: true }); results.overview = overviewSearch; } if (params.include_news) { const newsSearch = await this.search({ query: `"${params.company_name}" latest news announcements`, num_results: maxResults, type: 'neural', start_published_date: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], include_text: true, include_highlights: true }); results.news = newsSearch; } if (params.include_competitors) { const competitorSearch = await this.search({ query: `"${params.company_name}" competitors "competing with" OR "competes with" OR "alternative to"`, num_results: maxResults, type: 'neural', include_text: true, include_highlights: true }); results.competitors = competitorSearch; } if (params.include_reviews) { const reviewSearch = await this.search({ query: `"${params.company_name}" reviews customer feedback testimonials ratings`, num_results: maxResults, type: 'neural', include_text: true, include_highlights: true }); results.reviews = reviewSearch; } return results; } catch (error) { throw new ExaError( error instanceof Error ? error.message : 'Failed to research company', 'RESEARCH_ERROR' ); } } /** * Crawl and extract content from URLs */ async crawl(params: { urls: string[]; include_text?: boolean; include_highlights?: boolean; include_summary?: boolean; summary_query?: string; }) { try { const contents = await this.client.getContents( params.urls, { text: params.include_text ? { maxCharacters: 5000, includeHtmlTags: false } : undefined, highlights: params.include_highlights ? { numSentences: 5, highlightsPerUrl: 5, query: params.summary_query || "key information" } : undefined, summary: params.include_summary ? { query: params.summary_query || "main points and key information" } : undefined } ); return contents.results; } catch (error) { throw new ExaError( error instanceof Error ? error.message : 'Failed to crawl URLs', 'CRAWL_ERROR' ); } } /** * Search LinkedIn profiles and companies */ async searchLinkedIn(params: { query: string; search_type?: 'people' | 'companies' | 'both'; num_results?: number; include_bios?: boolean; include_summaries?: boolean; }) { try { const searchType = params.search_type || 'both'; const numResults = params.num_results || 10; const results: any = {}; if (searchType === 'people' || searchType === 'both') { const peopleSearch = await this.search({ query: `site:linkedin.com/in/ ${params.query}`, num_results: numResults, type: 'keyword', include_text: params.include_bios, include_summary: params.include_summaries }); results.people = peopleSearch; } if (searchType === 'companies' || searchType === 'both') { const companySearch = await this.search({ query: `site:linkedin.com/company/ ${params.query}`, num_results: numResults, type: 'keyword', include_text: params.include_bios, include_summary: params.include_summaries }); results.companies = companySearch; } return results; } catch (error) { throw new ExaError( error instanceof Error ? error.message : 'Failed to search LinkedIn', 'LINKEDIN_ERROR' ); } } /** * Start a deep research task (async) */ async startDeepResearch(params: { topic: string; research_type?: 'comprehensive' | 'news' | 'academic' | 'market'; max_results?: number; time_range?: 'week' | 'month' | 'quarter' | 'year' | 'all'; }) { const taskId = `research_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // Store task metadata (in production, this would be in a database) global.researchTasks = global.researchTasks || {}; global.researchTasks[taskId] = { status: 'in_progress', started_at: new Date().toISOString(), params: params }; // Simulate async research (in production, this would be a queue job) this.performDeepResearch(taskId, params).catch(error => { global.researchTasks[taskId].status = 'failed'; global.researchTasks[taskId].error = error.message; }); return { task_id: taskId, status: 'started', message: 'Deep research task initiated. Use deep_researcher_check to monitor progress.' }; } /** * Check deep research task status */ async checkDeepResearch(taskId: string) { global.researchTasks = global.researchTasks || {}; const task = global.researchTasks[taskId]; if (!task) { throw new ExaError('Task not found', 'TASK_NOT_FOUND'); } return { task_id: taskId, status: task.status, started_at: task.started_at, completed_at: task.completed_at, results: task.results, error: task.error }; } /** * Perform deep research (internal method) */ private async performDeepResearch(taskId: string, params: any) { try { const task = global.researchTasks[taskId]; const results: any = { summary: '', sections: [] }; // Determine time range let startDate: string | undefined; if (params.time_range && params.time_range !== 'all') { const now = new Date(); const ranges = { week: 7, month: 30, quarter: 90, year: 365 }; const days = ranges[params.time_range as keyof typeof ranges]; startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; } // Perform multiple searches based on research type const searchQueries = this.getResearchQueries(params.topic, params.research_type || 'comprehensive'); for (const [section, query] of Object.entries(searchQueries)) { const searchResults = await this.search({ query: query as string, num_results: Math.floor((params.max_results || 50) / Object.keys(searchQueries).length), type: 'neural', start_published_date: startDate, include_text: true, include_summary: true, include_highlights: true }); results.sections.push({ title: section, query: query, results: searchResults }); } // Generate summary results.summary = `Completed deep research on "${params.topic}" with ${results.sections.length} sections and ${results.sections.reduce((acc: number, s: any) => acc + s.results.length, 0)} total results.`; task.status = 'completed'; task.completed_at = new Date().toISOString(); task.results = results; } catch (error) { throw error; } } /** * Get research queries based on type */ private getResearchQueries(topic: string, type: string) { const queries: Record<string, Record<string, string>> = { comprehensive: { 'Overview': `"${topic}" overview introduction "what is"`, 'Recent Developments': `"${topic}" latest news recent developments updates`, 'Key Players': `"${topic}" companies organizations leaders "key players"`, 'Analysis': `"${topic}" analysis insights trends expert opinion`, 'Future Outlook': `"${topic}" future predictions forecast "what's next"` }, news: { 'Breaking News': `"${topic}" breaking news latest updates`, 'Industry News': `"${topic}" industry news sector updates`, 'Press Releases': `"${topic}" press release announcement official`, 'Media Coverage': `"${topic}" media coverage journalism reports` }, academic: { 'Research Papers': `"${topic}" research paper study academic journal`, 'Studies': `"${topic}" study findings results data`, 'Reviews': `"${topic}" literature review meta-analysis systematic`, 'Theories': `"${topic}" theory framework model academic` }, market: { 'Market Size': `"${topic}" market size revenue valuation statistics`, 'Competition': `"${topic}" competitors market share competitive landscape`, 'Trends': `"${topic}" market trends growth forecast analysis`, 'Opportunities': `"${topic}" opportunities challenges SWOT analysis` } }; return queries[type] || queries.comprehensive; } /** * Get the Exa client instance */ getClient(): any { return this.client; } } // Declare global type for research tasks declare global { var researchTasks: Record<string, any>; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/joerup/exa-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server