feat: add Exa AI-powered search tool (#266)

Adds Exa as an AI search integration for content research, competitor
discovery, link prospecting, news monitoring, and audience research.

- CLI (tools/clis/exa.js): zero-dep Node.js wrapper for /search,
  /findSimilar, and /contents with neural/auto/fast/deep search types,
  category + domain + text + date filtering, and composable content
  retrieval (text, highlights, summary) in a single request
- Integration guide (tools/integrations/exa.md): auth, endpoints,
  common agent operations, parameters, and linked skills
- Registry entry under new 'AI Search' category + MCP-enabled list
- Uses EXA_API_KEY env var with --dry-run support and masked credentials
This commit is contained in:
Teo
2026-05-18 14:05:40 -07:00
committed by GitHub
parent 75e8221af8
commit 4b37de5228
4 changed files with 306 additions and 0 deletions
+12
View File
@@ -74,6 +74,7 @@ Quick reference for AI agents to discover tool capabilities and integration meth
| introw | Partner Ecosystem | - | ✓ | - | - | [introw.md](integrations/introw.md) | | introw | Partner Ecosystem | - | ✓ | - | - | [introw.md](integrations/introw.md) |
| pendo | Product Analytics | ✓ | - | [](clis/pendo.js) | - | [pendo.md](integrations/pendo.md) | | pendo | Product Analytics | ✓ | - | [](clis/pendo.js) | - | [pendo.md](integrations/pendo.md) |
| similarweb | Competitive Intelligence | ✓ | - | [](clis/similarweb.js) | - | [similarweb.md](integrations/similarweb.md) | | similarweb | Competitive Intelligence | ✓ | - | [](clis/similarweb.js) | - | [similarweb.md](integrations/similarweb.md) |
| exa | AI Search | ✓ | ✓ | [](clis/exa.js) | ✓ | [exa.md](integrations/exa.md) |
| firehose | Competitive Intelligence | ✓ | - | - | - | [firehose.md](integrations/firehose.md) | | firehose | Competitive Intelligence | ✓ | - | - | - | [firehose.md](integrations/firehose.md) |
| sparktoro | Audience Research | - | - | - | - | [sparktoro.md](integrations/sparktoro.md) | | sparktoro | Audience Research | - | - | - | - | [sparktoro.md](integrations/sparktoro.md) |
| rb2b | Visitor Identification | ✓ | - | - | - | [rb2b.md](integrations/rb2b.md) | | rb2b | Visitor Identification | ✓ | - | - | - | [rb2b.md](integrations/rb2b.md) |
@@ -390,6 +391,16 @@ AI-powered content generation and optimization platforms.
**Agent recommendation**: AirOps for building AI content workflows that generate SEO-optimized content at scale. **Agent recommendation**: AirOps for building AI content workflows that generate SEO-optimized content at scale.
### AI Search
AI-powered web search APIs built for LLMs and agents. Return structured results with on-demand text, highlights, and summaries.
| Tool | Best For | Notes |
|------|----------|-------|
| **exa** | Neural/semantic web search, content research, competitor discovery | Search + findSimilar + Contents; MCP and SDKs available |
**Agent recommendation**: Exa for neural search over the open web — content research, competitor/similar-page discovery, link prospecting, news monitoring, and audience research. Pairs well with seo-audit, content-strategy, and competitor-profiling skills.
### Partner Ecosystem ### Partner Ecosystem
Partner data sharing, co-sell, and ecosystem management. Partner data sharing, co-sell, and ecosystem management.
@@ -471,6 +482,7 @@ These tools have Model Context Protocol servers available, enabling direct agent
- **outreach** - Sales engagement sequences - **outreach** - Sales engagement sequences
- **crossbeam** - Partner ecosystem data - **crossbeam** - Partner ecosystem data
- **introw** - Partner relationship management - **introw** - Partner relationship management
- **exa** - AI-powered web search for LLMs and agents
To use MCP tools, ensure the appropriate MCP server is configured in your environment. To use MCP tools, ensure the appropriate MCP server is configured in your environment.
+2
View File
@@ -50,6 +50,7 @@ Every CLI reads credentials from environment variables:
| `dataforseo` | `DATAFORSEO_LOGIN`, `DATAFORSEO_PASSWORD` | | `dataforseo` | `DATAFORSEO_LOGIN`, `DATAFORSEO_PASSWORD` |
| `demio` | `DEMIO_API_KEY`, `DEMIO_API_SECRET` | | `demio` | `DEMIO_API_KEY`, `DEMIO_API_SECRET` |
| `dub` | `DUB_API_KEY` | | `dub` | `DUB_API_KEY` |
| `exa` | `EXA_API_KEY` |
| `g2` | `G2_API_TOKEN` | | `g2` | `G2_API_TOKEN` |
| `ga4` | `GA4_ACCESS_TOKEN` | | `ga4` | `GA4_ACCESS_TOKEN` |
| `google-ads` | `GOOGLE_ADS_TOKEN`, `GOOGLE_ADS_DEVELOPER_TOKEN`, `GOOGLE_ADS_CUSTOMER_ID` | | `google-ads` | `GOOGLE_ADS_TOKEN`, `GOOGLE_ADS_DEVELOPER_TOKEN`, `GOOGLE_ADS_CUSTOMER_ID` |
@@ -148,6 +149,7 @@ DOMAINS=$(rewardful affiliates list | jq -r '.data[].email')
| `dataforseo.js` | SEO | [DataForSEO](https://dataforseo.com) | | `dataforseo.js` | SEO | [DataForSEO](https://dataforseo.com) |
| `demio.js` | Webinar | [Demio](https://demio.com) | | `demio.js` | Webinar | [Demio](https://demio.com) |
| `dub.js` | Links | [Dub.co](https://dub.co) | | `dub.js` | Links | [Dub.co](https://dub.co) |
| `exa.js` | AI Search | [Exa](https://exa.ai) |
| `g2.js` | Reviews | [G2](https://g2.com) | | `g2.js` | Reviews | [G2](https://g2.com) |
| `ga4.js` | Analytics | [Google Analytics 4](https://analytics.google.com) | | `ga4.js` | Analytics | [Google Analytics 4](https://analytics.google.com) |
| `google-ads.js` | Ads | [Google Ads](https://ads.google.com) | | `google-ads.js` | Ads | [Google Ads](https://ads.google.com) |
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env node
const API_KEY = process.env.EXA_API_KEY
const BASE_URL = 'https://api.exa.ai'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'EXA_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
if (args['dry-run']) {
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'x-api-key': '***', 'Content-Type': 'application/json', 'x-exa-integration': 'marketingskills' }, body: body || undefined }
}
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'x-api-key': API_KEY,
'Content-Type': 'application/json',
'x-exa-integration': 'marketingskills',
},
body: body ? JSON.stringify(body) : undefined,
})
const text = await res.text()
try {
return JSON.parse(text)
} catch {
return { status: res.status, body: text }
}
}
function parseArgs(args) {
const result = { _: [] }
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('--')) {
result[key] = next
i++
} else {
result[key] = true
}
} else {
result._.push(arg)
}
}
return result
}
function buildContents(args) {
const contents = {}
if (args.text) {
contents.text = args['max-chars']
? { maxCharacters: Number(args['max-chars']) }
: true
}
if (args.highlights) {
contents.highlights = args['highlight-query']
? { query: args['highlight-query'] }
: true
}
if (args.summary) {
contents.summary = args['summary-query']
? { query: args['summary-query'] }
: {}
}
return Object.keys(contents).length ? contents : null
}
const args = parseArgs(process.argv.slice(2))
const [cmd, ...rest] = args._
async function main() {
let result
switch (cmd) {
case 'search': {
const query = args.query || rest.join(' ')
if (!query) { result = { error: '--query required' }; break }
const body = { query }
if (args.type) body.type = args.type
if (args.num) body.numResults = Number(args.num)
if (args.category) body.category = args.category
if (args['include-domains']) body.includeDomains = args['include-domains'].split(',').map(s => s.trim())
if (args['exclude-domains']) body.excludeDomains = args['exclude-domains'].split(',').map(s => s.trim())
if (args['include-text']) body.includeText = args['include-text'].split(',').map(s => s.trim())
if (args['exclude-text']) body.excludeText = args['exclude-text'].split(',').map(s => s.trim())
if (args['start-published']) body.startPublishedDate = args['start-published']
if (args['end-published']) body.endPublishedDate = args['end-published']
if (args['start-crawl']) body.startCrawlDate = args['start-crawl']
if (args['end-crawl']) body.endCrawlDate = args['end-crawl']
if (args['user-location']) body.userLocation = args['user-location']
const contents = buildContents(args)
if (contents) body.contents = contents
result = await api('POST', '/search', body)
break
}
case 'find-similar': {
const url = args.url
if (!url) { result = { error: '--url required' }; break }
const body = { url }
if (args.num) body.numResults = Number(args.num)
if (args['include-domains']) body.includeDomains = args['include-domains'].split(',').map(s => s.trim())
if (args['exclude-domains']) body.excludeDomains = args['exclude-domains'].split(',').map(s => s.trim())
if (args['start-published']) body.startPublishedDate = args['start-published']
if (args['end-published']) body.endPublishedDate = args['end-published']
if (args['start-crawl']) body.startCrawlDate = args['start-crawl']
if (args['end-crawl']) body.endCrawlDate = args['end-crawl']
const contents = buildContents(args)
if (contents) body.contents = contents
result = await api('POST', '/findSimilar', body)
break
}
case 'contents': {
const urls = args.urls?.split(',').map(s => s.trim())
if (!urls || !urls.length) { result = { error: '--urls required (comma-separated)' }; break }
const body = { urls }
const contents = buildContents(args)
if (contents) Object.assign(body, contents)
else body.text = true
result = await api('POST', '/contents', body)
break
}
default:
result = {
error: 'Unknown command',
usage: {
search: 'search --query <q> [--type neural|fast|auto|deep-lite|deep|deep-reasoning|instant] [--num <n>] [--category company|research paper|news|personal site|financial report|people] [--include-domains <d1,d2>] [--exclude-domains <d1,d2>] [--include-text <phrases>] [--exclude-text <phrases>] [--start-published <ISO>] [--end-published <ISO>] [--user-location <CC>] [--text] [--highlights] [--summary] [--max-chars <n>] [--highlight-query <q>] [--summary-query <q>]',
'find-similar': 'find-similar --url <url> [--num <n>] [--include-domains <d1,d2>] [--exclude-domains <d1,d2>] [--start-published <ISO>] [--end-published <ISO>] [--text] [--highlights] [--summary]',
contents: 'contents --urls <url1,url2> [--text] [--highlights] [--summary] [--max-chars <n>] [--highlight-query <q>] [--summary-query <q>]',
options: '--dry-run (preview request without sending)',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})
+145
View File
@@ -0,0 +1,145 @@
# Exa
AI-powered web search API built for LLMs and agents. Returns high-quality search results with neural and keyword matching, plus on-demand content retrieval (full text, highlights, and summaries) in a single request.
## Capabilities
| Integration | Available | Notes |
|-------------|-----------|-------|
| API | ✓ | Search, Find Similar, Contents |
| MCP | ✓ | Official MCP server available |
| CLI | ✓ | [exa.js](../clis/exa.js) |
| SDK | ✓ | `exa-py` (Python), `exa-js` (TypeScript) |
## Authentication
- **Type**: API Key
- **Header**: `x-api-key: {key}`
- **Get key**: https://dashboard.exa.ai
## Endpoints
Base URL: `https://api.exa.ai`
| Endpoint | Purpose |
|----------|---------|
| `POST /search` | Search the web with neural, keyword-like, or auto-routed modes |
| `POST /findSimilar` | Find pages similar to a given URL |
| `POST /contents` | Fetch text, highlights, or summaries for one or more URLs |
## Common Agent Operations
### Web Search with Content
```bash
POST https://api.exa.ai/search
{
"query": "best B2B SaaS onboarding flows",
"type": "auto",
"numResults": 10,
"contents": {
"text": { "maxCharacters": 1000 },
"highlights": true
}
}
```
### Competitor Content Discovery
```bash
POST https://api.exa.ai/search
{
"query": "landing page teardowns",
"includeDomains": ["goodui.org", "growth.design", "marketingexamples.com"],
"startPublishedDate": "2024-01-01T00:00:00Z",
"contents": { "highlights": true }
}
```
### Find Similar Pages
```bash
POST https://api.exa.ai/findSimilar
{
"url": "https://stripe.com/pricing",
"numResults": 20,
"contents": { "summary": { "query": "What pricing model and price points does this page use?" } }
}
```
### Category-Filtered Search
```bash
POST https://api.exa.ai/search
{
"query": "DTC beauty brand raising Series A",
"category": "news",
"numResults": 25,
"startPublishedDate": "2024-06-01T00:00:00Z"
}
```
### Fetch Contents for Known URLs
```bash
POST https://api.exa.ai/contents
{
"urls": ["https://example.com/post-1", "https://example.com/post-2"],
"text": true,
"summary": { "query": "Summarize this article's key argument in one paragraph." }
}
```
## Key Parameters
### Search Types
- `auto` - Automatically routes between neural and keyword matching (default)
- `neural` - Embedding-based semantic search; best for concept/idea queries
- `fast` - Lower-latency neural search
- `instant` - Returns cached results near-instantly
- `deep-lite`, `deep`, `deep-reasoning` - Agentic search variants that plan multiple queries and synthesize
### Categories
`company`, `research paper`, `news`, `personal site`, `financial report`, `people`
### Filtering
- `includeDomains` / `excludeDomains` - Restrict to or exclude specific domains (up to 1200)
- `includeText` / `excludeText` - Require or forbid phrases in result pages
- `startPublishedDate` / `endPublishedDate` - ISO 8601 publication date range
- `startCrawlDate` / `endCrawlDate` - ISO 8601 crawl date range
- `userLocation` - Two-letter country code (e.g., `US`)
### Contents (Mix and Match)
All three can be requested in the same call:
- `text: true` or `{ maxCharacters, includeHtmlTags, verbosity }` - Full or truncated page text
- `highlights: true` or `{ query, maxCharacters }` - LLM-selected relevant snippets
- `summary: { query, schema }` - LLM-generated summary, optionally conforming to a JSON schema
## When to Use
- **Content research** - Find high-quality long-form content on niche topics by meaning, not just keywords
- **Competitor discovery** - Find companies similar to one you've identified via `findSimilar`
- **SEO content gap analysis** - Search for topics your competitors rank for and pull highlights for quick review
- **Customer research** - Find forum threads, blog posts, and reviews about your product or category
- **Audience research** - Discover blogs, newsletters, and communities where your ICP publishes or comments
- **News monitoring** - Track mentions of your brand, competitors, or category with date-filtered news search
- **Link prospecting** - Find authoritative pages covering topics you write about, for outreach
- **Lead research** - Use the `company` and `people` categories to discover accounts or individuals matching criteria
## Rate Limits
- Varies by plan; see https://exa.ai/pricing
- Most production plans support hundreds of concurrent requests
- Content retrieval (text/highlights/summary) is billed separately from the base search
## Relevant Skills
- seo-audit
- ai-seo
- content-strategy
- competitor-profiling
- competitor-alternatives
- customer-research
- cold-email
- lead-magnets
- marketing-ideas