#!/usr/bin/env node /** * Exa Search CLI - Wrapper for Exa MCP Server * * Usage: * ./exa-cli.mjs search "query" * ./exa-cli.mjs code "Python async patterns" * ./exa-cli.mjs company "Anthropic" * ./exa-cli.mjs people "CTO AI startups" * ./exa-cli.mjs crawl "https://example.com" * ./exa-cli.mjs research "topic for deep research" * ./exa-cli.mjs research-check */ const MCP_BASE = 'https://mcp.exa.ai/mcp'; const TOOLS_PARAM = 'tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check'; function getMcpUrl() { const apiKey = process.env.EXA_API_KEY; let url = `${MCP_BASE}?${TOOLS_PARAM}`; if (apiKey) { url += `&exaApiKey=${apiKey}`; } return url; } async function callMcp(toolName, args) { const url = getMcpUrl(); const request = { jsonrpc: '2.0', id: Date.now(), method: 'tools/call', params: { name: toolName, arguments: args } }; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', }, body: JSON.stringify(request) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${await response.text()}`); } const contentType = response.headers.get('content-type') || ''; // Handle SSE streaming response if (contentType.includes('text/event-stream')) { const text = await response.text(); const lines = text.split('\n'); let result = null; for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data && data !== '[DONE]') { try { const parsed = JSON.parse(data); if (parsed.result) { result = parsed.result; } else if (parsed.error) { throw new Error(`MCP Error: ${JSON.stringify(parsed.error)}`); } } catch (e) { // Skip non-JSON lines } } } } return result; } // Handle regular JSON response const result = await response.json(); if (result.error) { throw new Error(`MCP Error: ${JSON.stringify(result.error)}`); } return result.result; } function parseArgs(args) { const parsed = { _: [] }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg.startsWith('--')) { const key = arg.slice(2); const next = args[i + 1]; if (next && !next.startsWith('--')) { parsed[key] = next; i++; } else { parsed[key] = true; } } else { parsed._.push(arg); } } return parsed; } function formatContent(content) { if (!content) return ''; if (Array.isArray(content)) { return content.map(c => c.text || JSON.stringify(c)).join('\n'); } if (typeof content === 'string') return content; return JSON.stringify(content, null, 2); } async function main() { const args = parseArgs(process.argv.slice(2)); const command = args._[0]; const query = args._.slice(1).join(' '); const numResults = parseInt(args.num) || 10; if (!command) { console.log(`Exa Search CLI Commands: search Web search search-advanced Advanced search with filters code Code/documentation search crawl Get full page content company Company research people People/profile search research Start deep research research-check Check research status Options: --num Number of results (default: 10) --domains Comma-separated domains (advanced search) --after Results after date YYYY-MM-DD (advanced search) --before Results before date YYYY-MM-DD (advanced search) --tokens Token limit for code search (default: 5000) Environment: EXA_API_KEY Your Exa API key (optional, for higher rate limits) `); process.exit(0); } try { let result; switch (command) { case 'search': if (!query) throw new Error('Query required'); result = await callMcp('web_search_exa', { query, numResults }); break; case 'search-advanced': if (!query) throw new Error('Query required'); const advancedArgs = { query, numResults }; if (args.domains) { advancedArgs.includeDomains = args.domains.split(','); } if (args.after) { advancedArgs.startPublishedDate = args.after; } if (args.before) { advancedArgs.endPublishedDate = args.before; } result = await callMcp('web_search_advanced_exa', advancedArgs); break; case 'code': if (!query) throw new Error('Query required'); const tokensNum = parseInt(args.tokens) || 5000; result = await callMcp('get_code_context_exa', { query, tokensNum }); break; case 'crawl': if (!query) throw new Error('URL required'); result = await callMcp('crawling_exa', { url: query }); break; case 'company': if (!query) throw new Error('Company name required'); result = await callMcp('company_research_exa', { companyName: query }); break; case 'people': if (!query) throw new Error('Query required'); result = await callMcp('people_search_exa', { query, numResults }); break; case 'research': if (!query) throw new Error('Research topic required'); result = await callMcp('deep_researcher_start', { query }); break; case 'research-check': if (!query) throw new Error('Task ID required'); result = await callMcp('deep_researcher_check', { taskId: query }); break; default: console.error(`Unknown command: ${command}`); process.exit(1); } console.log(formatContent(result?.content || result)); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } } main();