Back to skills
SkillHub ClubAnalyze Data & AIFull StackData / AIIntegration

exa-rag

Build RAG pipelines with Exa.ai for real-time web retrieval. Use when building retrieval-augmented generation, integrating Exa with LangChain, LlamaIndex, Vercel AI SDK, or implementing AI agents with web search capabilities. Triggers on: RAG pipeline, retrieval augmented generation, Exa LangChain, Exa LlamaIndex, ExaSearchRetriever, ExaSearchResults, Exa MCP, Exa tool calling, Claude tool use, AI agent web search, grounded generation, citation generation, fact checking, hallucination detection, OpenAI compatibility, chat completions.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
2
Hot score
79
Updated
March 20, 2026
Overall rating
C1.1
Composite score
1.1
Best-practice grade
F39.6

Install command

npx @skill-hub/cli install ejirocodes-agent-skills-exa-rag

Repository

ejirocodes/agent-skills

Skill path: exa/skills/exa-rag

Build RAG pipelines with Exa.ai for real-time web retrieval. Use when building retrieval-augmented generation, integrating Exa with LangChain, LlamaIndex, Vercel AI SDK, or implementing AI agents with web search capabilities. Triggers on: RAG pipeline, retrieval augmented generation, Exa LangChain, Exa LlamaIndex, ExaSearchRetriever, ExaSearchResults, Exa MCP, Exa tool calling, Claude tool use, AI agent web search, grounded generation, citation generation, fact checking, hallucination detection, OpenAI compatibility, chat completions.

Open repository

Best for

Primary workflow: Analyze Data & AI.

Technical facets: Full Stack, Data / AI, Integration.

Target audience: everyone.

License: MIT.

Original source

Catalog source: SkillHub Club.

Repository owner: ejirocodes.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install exa-rag into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/ejirocodes/agent-skills before adding exa-rag to shared team environments
  • Use exa-rag for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: exa-rag
description: "Build RAG pipelines with Exa.ai for real-time web retrieval. Use when building retrieval-augmented generation, integrating Exa with LangChain, LlamaIndex, Vercel AI SDK, or implementing AI agents with web search capabilities. Triggers on: RAG pipeline, retrieval augmented generation, Exa LangChain, Exa LlamaIndex, ExaSearchRetriever, ExaSearchResults, Exa MCP, Exa tool calling, Claude tool use, AI agent web search, grounded generation, citation generation, fact checking, hallucination detection, OpenAI compatibility, chat completions."
license: MIT
metadata:
  author: ejirocodes
  version: '1.0.0'
---

# Exa RAG Integration

## Quick Reference

| Topic | When to Use | Reference |
|-------|-------------|-----------|
| **LangChain** | Building RAG chains with LangChain | [langchain.md](references/langchain.md) |
| **LlamaIndex** | Using Exa as a LlamaIndex data source | [llamaindex.md](references/llamaindex.md) |
| **Vercel AI SDK** | Adding web search to Next.js AI apps | [vercel-ai.md](references/vercel-ai.md) |
| **MCP & Tools** | Claude MCP server, OpenAI tools, function calling | [mcp-tools.md](references/mcp-tools.md) |

## Essential Patterns

### LangChain Retriever

```python
from langchain_exa import ExaSearchRetriever

retriever = ExaSearchRetriever(
    exa_api_key="your-key",
    k=5,
    highlights=True
)

docs = retriever.invoke("latest AI research papers")
```

### LlamaIndex Reader

```python
from llama_index.readers.web import ExaReader

reader = ExaReader(api_key="your-key")
documents = reader.load_data(
    query="machine learning best practices",
    num_results=10
)
```

### Vercel AI SDK Tool

```typescript
import { exa } from "@agentic/exa";
import { createOpenAI } from "@ai-sdk/openai";
import { generateText } from "ai";

const result = await generateText({
  model: openai("gpt-4"),
  tools: { search: exa.searchAndContents },
  prompt: "Search for the latest TypeScript features",
});
```

### OpenAI-Compatible Endpoint

```python
from openai import OpenAI

client = OpenAI(
    base_url="https://api.exa.ai/v1",
    api_key="your-exa-key"
)

response = client.chat.completions.create(
    model="exa",
    messages=[{"role": "user", "content": "What are the latest AI trends?"}]
)
```

## Integration Selection

| Framework | Best For | Key Feature |
|-----------|----------|-------------|
| **LangChain** | Complex chains, agents | ExaSearchRetriever, tool integration |
| **LlamaIndex** | Document indexing, Q&A | ExaReader, query engines |
| **Vercel AI SDK** | Next.js apps, streaming | Tool definitions, edge-ready |
| **OpenAI Compat** | Drop-in replacement | Minimal code changes |
| **Claude MCP** | Claude Desktop, Claude Code | Native tool calling |

## Common Mistakes

1. **Not using highlights for RAG** - Full text wastes context; use `highlights=True` for relevant snippets
2. **Missing source attribution** - Always include `result.url` in citations for grounded responses
3. **Ignoring summaries** - `summary=True` provides concise context without full page overhead
4. **Over-fetching results** - Start with 3-5 results; more isn't always better for RAG quality
5. **Not filtering domains** - Use `include_domains` to limit to authoritative sources
6. **Skipping date filters** - For current events, always add `start_published_date` to avoid stale info
7. **Forgetting async patterns** - Use async retrievers in production for better throughput


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/langchain.md

```markdown
# LangChain Integration Reference

## Table of Contents

- [Installation](#installation)
- [ExaSearchRetriever](#exasearchretriever)
- [ExaSearchResults Tool](#exasearchresults-tool)
- [RAG Chain Patterns](#rag-chain-patterns)
- [Agent Integration](#agent-integration)

---

## Installation

```bash
pip install langchain-exa
```

---

## ExaSearchRetriever

The primary way to use Exa with LangChain for RAG.

### Basic Usage

```python
from langchain_exa import ExaSearchRetriever

retriever = ExaSearchRetriever(
    exa_api_key="your-key",  # or set EXA_API_KEY env var
    k=5
)

docs = retriever.invoke("latest developments in quantum computing")
for doc in docs:
    print(f"URL: {doc.metadata['url']}")
    print(f"Title: {doc.metadata['title']}")
    print(f"Content: {doc.page_content[:200]}...")
```

### With Highlights

```python
retriever = ExaSearchRetriever(
    exa_api_key="your-key",
    k=5,
    highlights=True,
    num_sentences=3
)

docs = retriever.invoke("React Server Components best practices")
# docs contain highlight snippets instead of full text
```

### With Filters

```python
retriever = ExaSearchRetriever(
    exa_api_key="your-key",
    k=10,
    include_domains=["github.com", "stackoverflow.com"],
    start_published_date="2024-01-01",
    type="neural"
)
```

### Configuration Options

| Parameter | Type | Description |
|-----------|------|-------------|
| `exa_api_key` | str | API key (or use env var) |
| `k` | int | Number of results |
| `type` | str | "auto", "neural", or "keyword" |
| `include_domains` | list | Limit to these domains |
| `exclude_domains` | list | Exclude these domains |
| `start_published_date` | str | Start date filter (YYYY-MM-DD) |
| `end_published_date` | str | End date filter |
| `highlights` | bool | Return highlights instead of full text |
| `num_sentences` | int | Sentences per highlight |
| `text_length_limit` | int | Max characters for text |

---

## ExaSearchResults Tool

For LangChain agents that need web search as a tool.

### Basic Tool

```python
from langchain_exa import ExaSearchResults
from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain_openai import ChatOpenAI

# Create the tool
search_tool = ExaSearchResults(
    exa_api_key="your-key",
    max_results=5
)

# Use in an agent
llm = ChatOpenAI(model="gpt-4")
agent = create_openai_functions_agent(llm, [search_tool], prompt)
executor = AgentExecutor(agent=agent, tools=[search_tool])

result = executor.invoke({"input": "What are the latest AI news today?"})
```

### Tool with Custom Configuration

```python
search_tool = ExaSearchResults(
    exa_api_key="your-key",
    max_results=10,
    text_length_limit=1000,
    highlights=True,
    include_domains=["techcrunch.com", "wired.com"]
)
```

---

## RAG Chain Patterns

### Basic RAG Chain

```python
from langchain_exa import ExaSearchRetriever
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

retriever = ExaSearchRetriever(exa_api_key="your-key", k=5, highlights=True)
llm = ChatOpenAI(model="gpt-4")

prompt = ChatPromptTemplate.from_template("""
Answer the question based on the following context. Include source URLs.

Context:
{context}

Question: {question}

Answer:
""")

def format_docs(docs):
    return "\n\n".join(
        f"Source: {doc.metadata['url']}\n{doc.page_content}"
        for doc in docs
    )

chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

answer = chain.invoke("What are the key features of Python 3.12?")
```

### RAG with Citations

```python
from langchain_core.pydantic_v1 import BaseModel, Field
from typing import List

class Citation(BaseModel):
    url: str
    title: str
    snippet: str

class AnswerWithCitations(BaseModel):
    answer: str
    citations: List[Citation]

def create_cited_answer(docs, question):
    context = format_docs(docs)
    llm_with_structure = llm.with_structured_output(AnswerWithCitations)

    prompt = f"""
    Based on the context below, answer the question and cite your sources.

    Context:
    {context}

    Question: {question}
    """

    return llm_with_structure.invoke(prompt)
```

### Conversational RAG

```python
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationalRetrievalChain

retriever = ExaSearchRetriever(exa_api_key="your-key", k=5)
memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)

chain = ConversationalRetrievalChain.from_llm(
    llm=ChatOpenAI(model="gpt-4"),
    retriever=retriever,
    memory=memory
)

response = chain.invoke({"question": "What is LangChain?"})
response = chain.invoke({"question": "How does it compare to LlamaIndex?"})
```

---

## Agent Integration

### ReAct Agent with Exa

```python
from langchain_exa import ExaSearchResults
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
from langchain import hub

# Get ReAct prompt
prompt = hub.pull("hwchase17/react")

# Create tools
search = ExaSearchResults(exa_api_key="your-key", max_results=5)
tools = [search]

# Create agent
llm = ChatOpenAI(model="gpt-4")
agent = create_react_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

result = executor.invoke({
    "input": "Research the latest developments in AI agents and summarize the key trends"
})
```

### Multi-Tool Agent

```python
from langchain_exa import ExaSearchResults, ExaFindSimilar

search_tool = ExaSearchResults(
    exa_api_key="your-key",
    name="web_search",
    description="Search the web for current information"
)

similar_tool = ExaFindSimilar(
    exa_api_key="your-key",
    name="find_similar",
    description="Find pages similar to a given URL"
)

tools = [search_tool, similar_tool]
# Use with your preferred agent type
```

### LangGraph Integration

```python
from langgraph.graph import StateGraph, END
from langchain_exa import ExaSearchRetriever

retriever = ExaSearchRetriever(exa_api_key="your-key", k=5)

def search_node(state):
    query = state["query"]
    docs = retriever.invoke(query)
    return {"documents": docs}

def generate_node(state):
    docs = state["documents"]
    # Generate answer using docs
    return {"answer": answer}

# Build graph
workflow = StateGraph(dict)
workflow.add_node("search", search_node)
workflow.add_node("generate", generate_node)
workflow.set_entry_point("search")
workflow.add_edge("search", "generate")
workflow.add_edge("generate", END)

app = workflow.compile()
```

```

### references/llamaindex.md

```markdown
# LlamaIndex Integration Reference

## Table of Contents

- [Installation](#installation)
- [ExaReader](#exareader)
- [Query Engine Patterns](#query-engine-patterns)
- [Index Integration](#index-integration)
- [Advanced Patterns](#advanced-patterns)

---

## Installation

```bash
pip install llama-index-readers-web
```

---

## ExaReader

The primary way to load web content into LlamaIndex.

### Basic Usage

```python
from llama_index.readers.web import ExaReader

reader = ExaReader(api_key="your-key")  # or set EXA_API_KEY env var

documents = reader.load_data(
    query="machine learning best practices",
    num_results=10
)

for doc in documents:
    print(f"Source: {doc.metadata['url']}")
    print(f"Content: {doc.text[:200]}...")
```

### With Search Options

```python
documents = reader.load_data(
    query="React Server Components tutorial",
    num_results=10,
    include_domains=["react.dev", "nextjs.org"],
    start_published_date="2024-01-01",
    text_length_limit=2000
)
```

### With Highlights

```python
documents = reader.load_data(
    query="Python async patterns",
    num_results=10,
    highlights=True,
    num_sentences=3
)
# Returns documents with highlight snippets
```

### Configuration Options

| Parameter | Type | Description |
|-----------|------|-------------|
| `query` | str | Search query |
| `num_results` | int | Number of results (default: 10) |
| `include_domains` | list | Limit to these domains |
| `exclude_domains` | list | Exclude these domains |
| `start_published_date` | str | Start date (YYYY-MM-DD) |
| `end_published_date` | str | End date |
| `highlights` | bool | Return highlights |
| `num_sentences` | int | Sentences per highlight |
| `text_length_limit` | int | Max characters |

---

## Query Engine Patterns

### Basic Query Engine

```python
from llama_index.readers.web import ExaReader
from llama_index.core import VectorStoreIndex
from llama_index.llms.openai import OpenAI

# Load documents from Exa
reader = ExaReader(api_key="your-key")
documents = reader.load_data(
    query="GraphQL best practices",
    num_results=10
)

# Create index and query engine
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine(llm=OpenAI(model="gpt-4"))

# Query
response = query_engine.query("What are the key GraphQL patterns?")
print(response)
```

### Query Engine with Citations

```python
from llama_index.core import VectorStoreIndex
from llama_index.core.response_synthesizers import ResponseMode

documents = reader.load_data(query="kubernetes deployment", num_results=10)
index = VectorStoreIndex.from_documents(documents)

query_engine = index.as_query_engine(
    response_mode=ResponseMode.COMPACT,
    llm=OpenAI(model="gpt-4")
)

response = query_engine.query("How do I deploy to Kubernetes?")

# Access source nodes
for node in response.source_nodes:
    print(f"Source: {node.metadata['url']}")
    print(f"Score: {node.score}")
```

### Chat Engine

```python
from llama_index.core.chat_engine import CondenseQuestionChatEngine

documents = reader.load_data(query="LangChain vs LlamaIndex", num_results=10)
index = VectorStoreIndex.from_documents(documents)

chat_engine = CondenseQuestionChatEngine.from_defaults(
    query_engine=index.as_query_engine(),
    llm=OpenAI(model="gpt-4")
)

response = chat_engine.chat("What are the differences?")
response = chat_engine.chat("Which is better for RAG?")
```

---

## Index Integration

### Combining with Existing Index

```python
from llama_index.core import VectorStoreIndex, StorageContext
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb

# Existing Chroma collection
chroma_client = chromadb.Client()
collection = chroma_client.get_or_create_collection("my_collection")
vector_store = ChromaVectorStore(chroma_collection=collection)

# Load new documents from Exa
reader = ExaReader(api_key="your-key")
new_docs = reader.load_data(query="latest AI news", num_results=5)

# Add to existing index
storage_context = StorageContext.from_defaults(vector_store=vector_store)
index = VectorStoreIndex.from_documents(
    new_docs,
    storage_context=storage_context
)
```

### Refresh with Real-Time Data

```python
def refresh_index_with_exa(index, query, num_results=5):
    """Add fresh web content to an existing index."""
    reader = ExaReader(api_key="your-key")
    fresh_docs = reader.load_data(
        query=query,
        num_results=num_results,
        start_published_date="2024-01-01"  # Recent only
    )

    # Insert into existing index
    for doc in fresh_docs:
        index.insert(doc)

    return index
```

---

## Advanced Patterns

### Parallel Loading

```python
import asyncio
from llama_index.readers.web import ExaReader

async def load_multiple_queries(queries):
    reader = ExaReader(api_key="your-key")
    tasks = [
        asyncio.to_thread(
            reader.load_data,
            query=q,
            num_results=5
        )
        for q in queries
    ]
    results = await asyncio.gather(*tasks)
    # Flatten documents
    return [doc for docs in results for doc in docs]

queries = ["Python async", "Python typing", "Python testing"]
all_docs = asyncio.run(load_multiple_queries(queries))
```

### Custom Node Parser

```python
from llama_index.core.node_parser import SentenceSplitter
from llama_index.readers.web import ExaReader

reader = ExaReader(api_key="your-key")
documents = reader.load_data(query="deep learning", num_results=10)

# Custom chunking for web content
parser = SentenceSplitter(
    chunk_size=512,
    chunk_overlap=50
)

nodes = parser.get_nodes_from_documents(documents)

# Create index from nodes
index = VectorStoreIndex(nodes)
```

### Sub-Question Query Engine

```python
from llama_index.core.query_engine import SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool

reader = ExaReader(api_key="your-key")

# Create specialized indexes
ml_docs = reader.load_data(query="machine learning frameworks", num_results=10)
web_docs = reader.load_data(query="web development frameworks", num_results=10)

ml_index = VectorStoreIndex.from_documents(ml_docs)
web_index = VectorStoreIndex.from_documents(web_docs)

# Create tools
ml_tool = QueryEngineTool.from_defaults(
    query_engine=ml_index.as_query_engine(),
    name="ml_search",
    description="Search for machine learning information"
)

web_tool = QueryEngineTool.from_defaults(
    query_engine=web_index.as_query_engine(),
    name="web_search",
    description="Search for web development information"
)

# Sub-question engine
query_engine = SubQuestionQueryEngine.from_defaults(
    query_engine_tools=[ml_tool, web_tool],
    llm=OpenAI(model="gpt-4")
)

response = query_engine.query(
    "Compare ML frameworks with web frameworks in terms of learning curve"
)
```

### Metadata Filtering

```python
from llama_index.core.vector_stores import MetadataFilters, FilterCondition

documents = reader.load_data(query="tech news", num_results=20)
index = VectorStoreIndex.from_documents(documents)

# Query with metadata filter
filters = MetadataFilters.from_dicts([
    {"key": "domain", "value": "techcrunch.com"}
])

query_engine = index.as_query_engine(filters=filters)
response = query_engine.query("What's the latest from TechCrunch?")
```

```

### references/vercel-ai.md

```markdown
# Vercel AI SDK Integration Reference

## Table of Contents

- [Installation](#installation)
- [Tool Definition](#tool-definition)
- [generateText Patterns](#generatetext-patterns)
- [Streaming Patterns](#streaming-patterns)
- [Next.js Integration](#nextjs-integration)

---

## Installation

```bash
npm install ai @ai-sdk/openai @agentic/exa
# or
pnpm add ai @ai-sdk/openai @agentic/exa
```

---

## Tool Definition

### Using @agentic/exa

```typescript
import { exa } from "@agentic/exa";

// Pre-built tool definitions
const searchTool = exa.searchAndContents;
const findSimilarTool = exa.findSimilarAndContents;
```

### Custom Tool Definition

```typescript
import { tool } from "ai";
import Exa from "exa-js";
import { z } from "zod";

const exaClient = new Exa(process.env.EXA_API_KEY);

const searchWeb = tool({
  description: "Search the web for information",
  parameters: z.object({
    query: z.string().describe("The search query"),
    numResults: z.number().default(5).describe("Number of results"),
  }),
  execute: async ({ query, numResults }) => {
    const results = await exaClient.searchAndContents(query, {
      numResults,
      text: true,
      highlights: true,
    });
    return results.results.map((r) => ({
      title: r.title,
      url: r.url,
      content: r.highlights?.join("\n") || r.text?.slice(0, 500),
    }));
  },
});
```

---

## generateText Patterns

### Basic Text Generation with Search

```typescript
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { exa } from "@agentic/exa";

const result = await generateText({
  model: openai("gpt-4"),
  tools: { search: exa.searchAndContents },
  maxSteps: 5,
  prompt: "What are the latest developments in AI agents?",
});

console.log(result.text);
```

### With Custom System Prompt

```typescript
const result = await generateText({
  model: openai("gpt-4"),
  tools: { search: exa.searchAndContents },
  system: `You are a research assistant. When searching, always:
1. Use specific queries
2. Include sources in your response
3. Synthesize information from multiple results`,
  prompt: "Research the current state of WebAssembly adoption",
});
```

### Multiple Tools

```typescript
import { exa } from "@agentic/exa";

const result = await generateText({
  model: openai("gpt-4"),
  tools: {
    search: exa.searchAndContents,
    findSimilar: exa.findSimilarAndContents,
  },
  maxSteps: 10,
  prompt:
    "Find articles about React Server Components and then find similar articles",
});
```

---

## Streaming Patterns

### Basic Streaming

```typescript
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { exa } from "@agentic/exa";

const result = await streamText({
  model: openai("gpt-4"),
  tools: { search: exa.searchAndContents },
  maxSteps: 5,
  prompt: "Summarize the latest TypeScript features",
});

for await (const chunk of result.textStream) {
  process.stdout.write(chunk);
}
```

### With Tool Call Handling

```typescript
import { streamText } from "ai";

const result = await streamText({
  model: openai("gpt-4"),
  tools: { search: exa.searchAndContents },
  maxSteps: 5,
  prompt: "Research quantum computing applications",
  onStepFinish: ({ stepType, toolCalls, toolResults }) => {
    if (stepType === "tool-result") {
      console.log("Search completed:", toolResults);
    }
  },
});
```

---

## Next.js Integration

### API Route (App Router)

```typescript
// app/api/chat/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { exa } from "@agentic/exa";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = await streamText({
    model: openai("gpt-4"),
    tools: { search: exa.searchAndContents },
    maxSteps: 5,
    messages,
  });

  return result.toDataStreamResponse();
}
```

### Client Component

```typescript
// components/Chat.tsx
"use client";

import { useChat } from "ai/react";

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } =
    useChat({
      api: "/api/chat",
    });

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>
          <strong>{m.role}:</strong> {m.content}
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Ask anything..."
          disabled={isLoading}
        />
        <button type="submit" disabled={isLoading}>
          Send
        </button>
      </form>
    </div>
  );
}
```

### With Tool Result Display

```typescript
// components/ChatWithTools.tsx
"use client";

import { useChat } from "ai/react";

export function ChatWithTools() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>
          <strong>{m.role}:</strong>
          {m.content}

          {/* Display tool invocations */}
          {m.toolInvocations?.map((tool, i) => (
            <div key={i} className="tool-result">
              <small>Searched: {tool.args.query}</small>
              {tool.state === "result" && (
                <ul>
                  {tool.result.map((r: any, j: number) => (
                    <li key={j}>
                      <a href={r.url}>{r.title}</a>
                    </li>
                  ))}
                </ul>
              )}
            </div>
          ))}
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}
```

### Edge Runtime Support

```typescript
// app/api/search/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { exa } from "@agentic/exa";

export const runtime = "edge";

export async function POST(req: Request) {
  const { query } = await req.json();

  const result = await streamText({
    model: openai("gpt-4"),
    tools: { search: exa.searchAndContents },
    maxSteps: 3,
    prompt: query,
  });

  return result.toDataStreamResponse();
}
```

---

## Error Handling

```typescript
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { exa } from "@agentic/exa";

try {
  const result = await generateText({
    model: openai("gpt-4"),
    tools: { search: exa.searchAndContents },
    prompt: "Search for...",
  });
} catch (error) {
  if (error.name === "AI_ToolExecutionError") {
    console.error("Search failed:", error.message);
    // Handle Exa API errors
  } else {
    throw error;
  }
}
```

---

## Best Practices

### Limit Max Steps

```typescript
// Prevent infinite tool loops
const result = await generateText({
  model: openai("gpt-4"),
  tools: { search: exa.searchAndContents },
  maxSteps: 5, // Reasonable limit
  prompt: "...",
});
```

### Cache Search Results

```typescript
import { unstable_cache } from "next/cache";

const cachedSearch = unstable_cache(
  async (query: string) => {
    const exa = new Exa(process.env.EXA_API_KEY);
    return exa.searchAndContents(query, { numResults: 5, text: true });
  },
  ["exa-search"],
  { revalidate: 3600 } // 1 hour
);
```

### Type-Safe Results

```typescript
import { z } from "zod";

const SearchResultSchema = z.object({
  title: z.string(),
  url: z.string(),
  content: z.string(),
});

const searchTool = tool({
  description: "Search the web",
  parameters: z.object({ query: z.string() }),
  execute: async ({ query }) => {
    const results = await exaClient.searchAndContents(query, {
      numResults: 5,
      text: true,
    });
    return results.results.map((r) =>
      SearchResultSchema.parse({
        title: r.title,
        url: r.url,
        content: r.text?.slice(0, 500) || "",
      })
    );
  },
});
```

```

### references/mcp-tools.md

```markdown
# MCP & Tool Calling Reference

## Table of Contents

- [Claude MCP Server](#claude-mcp-server)
- [OpenAI Tool Calling](#openai-tool-calling)
- [OpenAI Compatibility Mode](#openai-compatibility-mode)
- [Anthropic Tool Use](#anthropic-tool-use)
- [CrewAI Integration](#crewai-integration)

---

## Claude MCP Server

Exa provides an MCP (Model Context Protocol) server for Claude Desktop and Claude Code.

### Installation

```bash
npm install -g @anthropic/mcp-exa
# or
npx @anthropic/mcp-exa
```

### Claude Desktop Configuration

Add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "exa": {
      "command": "npx",
      "args": ["@anthropic/mcp-exa"],
      "env": {
        "EXA_API_KEY": "your-api-key"
      }
    }
  }
}
```

### Available MCP Tools

| Tool | Description |
|------|-------------|
| `exa_search` | Search the web with neural/keyword modes |
| `exa_search_and_contents` | Search and retrieve page contents |
| `exa_find_similar` | Find pages similar to a URL |
| `exa_get_contents` | Get contents for known URLs |

### Claude Code Usage

Once configured, Claude can use Exa directly:

```
User: Search for the latest React 19 features

Claude: I'll search for that using Exa.
[Uses exa_search_and_contents tool]
Based on the search results, React 19 includes...
```

---

## OpenAI Tool Calling

### Function Definition

```python
import openai
from exa_py import Exa

exa = Exa()

tools = [
    {
        "type": "function",
        "function": {
            "name": "web_search",
            "description": "Search the web for current information",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The search query"
                    },
                    "num_results": {
                        "type": "integer",
                        "description": "Number of results (default: 5)"
                    }
                },
                "required": ["query"]
            }
        }
    }
]

def handle_tool_call(tool_call):
    if tool_call.function.name == "web_search":
        args = json.loads(tool_call.function.arguments)
        results = exa.search_and_contents(
            args["query"],
            num_results=args.get("num_results", 5),
            text=True,
            highlights=True
        )
        return json.dumps([{
            "title": r.title,
            "url": r.url,
            "content": r.highlights or [r.text[:500]]
        } for r in results.results])
```

### Complete Example

```python
import openai
import json
from exa_py import Exa

client = openai.OpenAI()
exa = Exa()

def chat_with_search(user_message):
    messages = [{"role": "user", "content": user_message}]

    response = client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        tools=tools
    )

    # Handle tool calls
    while response.choices[0].message.tool_calls:
        tool_calls = response.choices[0].message.tool_calls
        messages.append(response.choices[0].message)

        for tool_call in tool_calls:
            result = handle_tool_call(tool_call)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result
            })

        response = client.chat.completions.create(
            model="gpt-4",
            messages=messages,
            tools=tools
        )

    return response.choices[0].message.content

answer = chat_with_search("What are the latest AI developments?")
```

---

## OpenAI Compatibility Mode

Exa provides an OpenAI-compatible endpoint for drop-in replacement.

### Basic Usage

```python
from openai import OpenAI

# Point to Exa's OpenAI-compatible endpoint
client = OpenAI(
    base_url="https://api.exa.ai/v1",
    api_key="your-exa-api-key"
)

response = client.chat.completions.create(
    model="exa",
    messages=[
        {"role": "user", "content": "What are the latest trends in AI?"}
    ]
)

print(response.choices[0].message.content)
```

### With Streaming

```python
stream = client.chat.completions.create(
    model="exa",
    messages=[
        {"role": "user", "content": "Summarize recent AI news"}
    ],
    stream=True
)

for chunk in stream:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="")
```

### TypeScript

```typescript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.exa.ai/v1",
  apiKey: process.env.EXA_API_KEY,
});

const response = await client.chat.completions.create({
  model: "exa",
  messages: [{ role: "user", content: "What's happening in tech?" }],
});
```

---

## Anthropic Tool Use

### Tool Definition

```python
import anthropic
from exa_py import Exa

client = anthropic.Anthropic()
exa = Exa()

tools = [
    {
        "name": "web_search",
        "description": "Search the web for information using Exa",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query"
                },
                "num_results": {
                    "type": "integer",
                    "description": "Number of results",
                    "default": 5
                }
            },
            "required": ["query"]
        }
    }
]
```

### Complete Flow

```python
def chat_with_claude_and_exa(user_message):
    messages = [{"role": "user", "content": user_message}]

    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=4096,
        tools=tools,
        messages=messages
    )

    # Handle tool use
    while response.stop_reason == "tool_use":
        tool_use = next(
            block for block in response.content
            if block.type == "tool_use"
        )

        # Execute Exa search
        results = exa.search_and_contents(
            tool_use.input["query"],
            num_results=tool_use.input.get("num_results", 5),
            text=True,
            highlights=True
        )

        tool_result = [{
            "title": r.title,
            "url": r.url,
            "content": r.highlights or [r.text[:500]]
        } for r in results.results]

        # Continue conversation
        messages = [
            {"role": "user", "content": user_message},
            {"role": "assistant", "content": response.content},
            {
                "role": "user",
                "content": [{
                    "type": "tool_result",
                    "tool_use_id": tool_use.id,
                    "content": json.dumps(tool_result)
                }]
            }
        ]

        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=4096,
            tools=tools,
            messages=messages
        )

    return response.content[0].text
```

---

## CrewAI Integration

### Installation

```bash
pip install crewai crewai-tools
```

### Exa Tool

```python
from crewai import Agent, Task, Crew
from crewai_tools import ExaSearchTool

# Create Exa search tool
search_tool = ExaSearchTool(
    api_key="your-exa-key",
    n_results=5
)

# Create agent with Exa
researcher = Agent(
    role="Research Analyst",
    goal="Research and summarize topics thoroughly",
    backstory="Expert researcher with web search capabilities",
    tools=[search_tool],
    verbose=True
)

# Create task
research_task = Task(
    description="Research the latest developments in AI agents",
    agent=researcher,
    expected_output="A comprehensive summary with sources"
)

# Run crew
crew = Crew(
    agents=[researcher],
    tasks=[research_task]
)

result = crew.kickoff()
```

### Multi-Agent with Exa

```python
from crewai import Agent, Task, Crew, Process

search_tool = ExaSearchTool(api_key="your-key")

# Research agent
researcher = Agent(
    role="Researcher",
    goal="Find relevant information",
    tools=[search_tool]
)

# Writer agent
writer = Agent(
    role="Writer",
    goal="Create well-written content from research"
)

# Tasks
research = Task(
    description="Research {topic}",
    agent=researcher
)

write = Task(
    description="Write article based on research",
    agent=writer,
    context=[research]
)

crew = Crew(
    agents=[researcher, writer],
    tasks=[research, write],
    process=Process.sequential
)

result = crew.kickoff(inputs={"topic": "AI trends 2024"})
```

---

## Google ADK Integration

```python
from google.adk import Agent
from exa_py import Exa

exa = Exa()

def exa_search(query: str, num_results: int = 5) -> str:
    """Search the web using Exa."""
    results = exa.search_and_contents(
        query,
        num_results=num_results,
        text=True,
        highlights=True
    )
    return "\n\n".join([
        f"Title: {r.title}\nURL: {r.url}\nContent: {r.highlights or r.text[:500]}"
        for r in results.results
    ])

agent = Agent(
    tools=[exa_search],
    model="gemini-1.5-pro"
)

response = agent.run("What are the latest AI developments?")
```

```