Skip to content
Community listing

Exa Search MCP

v3.0.0 changelog · 3

io.github.exa-labs/exa-mcp-server

Neural web search built for agents: web search with live crawling, code-context search, company research and page crawling. Needs an Exa API key.

transport:stdio runtime:npm license:MIT tools:4

Target client

run in your project directory

Held in this page only — never stored, logged, or sent anywhere but back to your screen.

claude mcp add exa-mcp-server -e EXA_API_KEY=<EXA_API_KEY> -- npx -y exa-mcp-server

setup guides: Claude Code Claude Desktop Cursor VS Code Zed Windsurf Cline Gemini CLI Grok LangChain

Adds it for this project only. Append --scope user to make it available everywhere. Claude Code docs

web_search_exa

web search exa

get_code_context_exa Read-only

get code context exa

company_research_exa

company research exa

crawling_exa

crawling exa

All 4 tools, one page each

Mutating and Read-only are read off each tool's name, not its schema — a hint, not a guarantee. Check the server's own documentation before granting access.

Technical article

Generated September 19, 2026

Exa MCP Server: The Complete Technical Guide to Web Search, Page Fetching and Agent Research over the Model Context Protocol

Introduction

The Exa MCP server connects an AI client to Exa, a search API built for AI agents. Exa describes itself as a “search API for AI agents that need real-time web data, deep research, and structured content from one endpoint.” The MCP server is the piece that turns that API into tools a coding agent or chat assistant can call: one tool to search the web and get back clean, ready-to-use content, one tool to read a page as markdown, and two optional tools for advanced filtered search and for multi-step research runs.

On the MCP Registry the server is listed as io.github.exa-labs/exa-mcp-server, titled “Exa Search”, published by Exa Labs under the MIT license. If you are new to the protocol itself, the registry’s MCP guides explain clients, servers, tools, resources and prompts. This article assumes you know roughly what an MCP server is and wants to know something narrower and more useful: what this particular server does, exactly how it behaves, how to connect it from the clients you use, and where it can bite you.

How this guide was put together

Everything below is drawn from the project’s own material rather than from folklore. The sources are the public repository at github.com/exa-labs/exa-mcp-server (its README, server.json, package.json, env.example, Dockerfile, the TypeScript source under src/ and api/, and the bundled agent skills under skills/) and the page metadata on exa.ai. Where the guide describes behavior, it is describing what the source code does at version 3.4.1, the version declared in the repository’s package.json and server.json when this was written.

Two honest caveats before you go further.

First, the registry listing lags the repository. The catalogue entry that the MCP Registry mirrors lists version 3.0.0, four tools named web_search_exa, get_code_context_exa, company_research_exa and crawling_exa, and marks EXA_API_KEY as a required secret. The repository has moved on. In the current source, the default tools are web_search_exa and web_fetch_exa; get_code_context_exa, company_research_exa and crawling_exa still exist but are flagged as deprecated and disabled by default; and the hosted server works without any key at all, within rate limits. This guide follows the repository, and it tells you when something you may have seen elsewhere (an older tutorial, a cached listing, an old config snippet) no longer matches.

Second, the illustrative examples are illustrative. Where you see a sample prompt, a sample tool call or a sample response shape, it exists to show structure. The response formats are taken from the code that assembles them, but the values inside them are placeholders. Nothing in this guide is a benchmark, and it makes no claims about result quality, latency or cost, because the repository does not contain measurements that would support them.

Who this guide is for

  • Developers wiring Exa into a coding agent such as Claude Code, Cursor, VS Code, Codex, Gemini CLI, Windsurf or Zed, who want to know which URL or command to use and which tools they will actually get.
  • Agent builders who call MCP servers programmatically and need the tool schemas, the error behavior and the timeouts spelled out.
  • Platform and security teams deciding whether a hosted search server belongs in their approved list, who care about authentication, rate limiting, what headers get sent, and how untrusted web content flows back into a model.
  • People who searched for “MCP web search” and are choosing between several servers. The comparison section is written for you.

Exa MCP at a glance

Item Value (from the repository at v3.4.1)
Publisher Exa Labs
License MIT
Registry name io.github.exa-labs/exa-mcp-server
Source https://github.com/exa-labs/exa-mcp-server
npm package exa-mcp-server (binary of the same name)
Hosted endpoint https://mcp.exa.ai/mcp
Transports Hosted HTTP endpoint; local stdio; self-hosted HTTP runtime
Runtime for local use Node.js 20 or newer
Default tools web_search_exa, web_fetch_exa
Optional tools web_search_advanced_exa, agent_run
Deprecated tools get_code_context_exa, company_research_exa, crawling_exa, deep_researcher_start, deep_researcher_check, people_search_exa, linkedin_search_exa, deep_search_exa
Authentication Anonymous (rate limited), OAuth, or an Exa API key
Prompts web_search_help, plus agent_research_help when an agent tool is enabled
Resources exa://tools/list, plus two agent resources when an agent tool is enabled
Bundled skills search and exa-agent

A one-minute orientation

If you only want it running, this is the shortest path, and the rest of the guide explains why it works and what to change.

Point any MCP client that supports remote servers at the hosted endpoint:

{
  "mcpServers": {
    "exa": {
      "type": "streamable-http",
      "url": "https://mcp.exa.ai/mcp"
    }
  }
}

That gives you web_search_exa and web_fetch_exa immediately, with anonymous rate limits. To lift the limits, add your own key to the URL as ?exaApiKey=YOUR_KEY, or better, let the client run the OAuth sign-in flow. To run the same server as a local process instead, use npx -y exa-mcp-server with EXA_API_KEY in the environment. Every one of those statements is unpacked, with the relevant source behind it, in the sections that follow.

Map of the rest of the guide

The Architecture section explains the three ways to run the server, the code path a tool call takes, the tool registry that decides which tools you see, and the authentication and rate-limit design. The Features section is a reference for every tool, parameter by parameter. The Integration section covers client configuration, programmatic use through the MCP SDKs, and self-hosting. The Use cases section turns the tools into workflows, borrowing heavily from the search guidance Exa ships inside the repository. Troubleshooting is organized by symptom. Comparison helps you choose between Exa and other ways of giving an agent web access. Advanced topics covers self-hosting, migration from older tool names, tool-set design and agent-run continuation. If you are looking for other servers to pair with this one, the MCP Registry server catalogue lets you search and filter the whole ecosystem.

Architecture

Three ways to run the same server

The most useful thing to understand about this project’s architecture is that there is one server implementation with three deployment shapes. All three share the same tool registration code, which is why behavior is consistent across them. What differs is where the process runs, how it authenticates, and which configuration knobs exist.

1. The hosted endpoint

The repository README leads with the hosted option: “Connect to Exa’s hosted MCP server” at https://mcp.exa.ai/mcp. There is nothing to install. Your MCP client speaks HTTP to Exa’s infrastructure, which runs the tool logic and calls the Exa search API on your behalf.

The hosted deployment is implemented as an HTTP handler in api/mcp.ts, built on the mcp-handler library (it appears in the dependency list) and written as a Vercel-style function. The source comments describe it directly: initializeMcpServer is “called by both the Vercel Function (api/mcp.ts) and the stdio entry (src/stdio.ts).” The same file contains the rate-limiting logic for anonymous users, the credential-priority rules and the OAuth challenge responses. The repository also contains small handlers for .well-known documents (an MCP configuration document, an OAuth protected-resource document, and an OpenAI apps challenge file), which is what lets clients discover the OAuth flow automatically.

The hosted endpoint is configured through its URL. Query parameters are the control surface:

  • ?tools=web_search_exa,web_fetch_exa,agent_run chooses which tools are registered. The README is explicit that this list replaces the defaults, so you must name every tool you want, not just the extra one.
  • ?exaApiKey=... supplies your own Exa API key.
  • ?login forces the OAuth login flow, which is useful for shared connectors and plugins. The path https://mcp.exa.ai/mcp/oauth does the same.

The server.json file, which is what registries such as the MCP Registry consume, declares this endpoint as a remote with type sse and a description that mentions the optional API key and the ?tools= parameter. The README, on the other hand, shows client configuration with "type": "streamable-http". In practice you should follow the README when configuring a client: current MCP clients that support remote servers expect the Streamable HTTP transport, and the README’s snippets are what Exa itself tells people to use. If a client offers only an SSE option, try the same URL, but prefer Streamable HTTP wherever it is available.

2. Local stdio via npm

The second shape is the classic local MCP server: a process your client launches, talking over standard input and output. The npm package is exa-mcp-server, and its bin entry points at dist/stdio.cjs. The build script bundles src/stdio-cli.ts into that single file using esbuild, targeting Node 20, in CommonJS format, with a #!/usr/bin/env node banner, and marks it executable. The files list in package.json ships only dist/stdio.cjs, the skills directory, plugin.json and mcp.json, so an install is small: one bundled script plus the agent skills.

The entry point, src/stdio.ts, is short enough to describe fully. It reads its configuration from environment variables:

Variable Purpose
EXA_API_KEY Your Exa API key. If set, the server treats the key as user-provided.
ENABLED_TOOLS or TOOLS Comma-separated tool IDs. When set, replaces the default tool set.
DEBUG Set to true for debug logging.
DEFAULT_SEARCH_TYPE One of auto, fast, instant. Sets the default search type for web_search_exa.
EXA_SOURCE An integration label that is attached to outgoing requests.
MCP_SESSION_ID A session identifier that is attached to outgoing requests.
MCP_MAX_DURATION_SECONDS Upper bound used to compute how long agent_run waits inside one call.
AGENT_CALL_WINDOW_MS Explicit agent_run wait window, capped by the maximum duration.

The server it constructs identifies itself with the name exa-search-server, the title Exa, the website https://exa.ai and a 32-pixel favicon, so clients that render server metadata can show a proper name and icon. It then calls initializeMcpServer(server, config) and connects a StdioServerTransport.

Note the tool-list parsing. parseTools splits the comma-separated string, trims each entry, drops empties, and runs the result through expandToolSelection, which understands two things beyond plain tool IDs: an alias, agent_tools, that expands to every tool in the agent group (currently just agent_run), and a set of legacy names (agent_create_run, agent_run_stream, agent_wait_for_run, agent_get_run_output, agent_cancel_run) that are silently mapped to the same alias so that old URLs and configs keep working. Unknown names are dropped without an error, a detail that matters when you are debugging why a tool did not appear. If parsing yields an empty list, the server falls back to the defaults.

3. A self-hosted HTTP runtime

The third shape is for teams that want to run the HTTP server themselves. The repository includes a Dockerfile and a src/runtime-server.ts entry point, with npm run build:runtime (a plain TypeScript compile) and npm run start:runtime (node dist/src/runtime-server.js). The Dockerfile is a two-stage build on node:22-bookworm-slim: the first stage installs dependencies with npm ci --ignore-scripts, copies api, src and tsconfig.json, and compiles; the second stage installs only production dependencies, copies the compiled output and the skills directory, creates a non-root system user (uid and gid 10001), sets HOST=0.0.0.0 and PORT=8000, exposes port 8000, and starts the runtime server.

Two practical observations follow from that Dockerfile. It runs as a non-root user by default, which is the right baseline for a container that will face the network. And it includes the skills directory in the image, because the agent-related resources and prompts read the bundled skill text at startup when an agent tool is enabled.

The shared core: one function registers everything

All three shapes converge on initializeMcpServer(server, config) in src/mcp-handler.ts. Reading this function is the fastest way to understand what the server exposes, so it is worth walking through.

The function first defines two small predicates. shouldRegisterTool(toolId) answers whether a tool is selected: if the config carries an explicit enabledTools list, the tool must be in it; otherwise the tool’s enabled flag in the registry decides. canRegisterTool(toolId) adds a second condition: if the tool is marked as requiring a user-provided API key, the server refuses to register it unless the config says the key was user-provided. That second check is why agent_run does not appear in a keyless local stdio session even if you name it in ENABLED_TOOLS. The hosted HTTP handler is stricter still: if a request names a key-requiring tool without presenting any credentials, it answers with an HTTP 401 and an OAuth challenge instead of quietly leaving the tool out, which is how OAuth-capable clients get prompted to sign in. The registry marks agent_run (and the deprecated deep-research and deep-search tools) with requiresUserProvidedApiKey.

Then comes a flat sequence of if (canRegisterTool(...)) blocks, one per tool, each calling the tool’s register...Tool function and appending the ID to a registeredTools list. The order is web_search_exa, web_search_advanced_exa, company_research_exa, web_fetch_exa, crawling_exa, people_search_exa, linkedin_search_exa, deep_researcher_start, deep_researcher_check, get_code_context_exa, deep_search_exa, agent_run. Two details are worth noticing. The deprecated crawling_exa is registered by calling the same registerWebFetchTool function under a different name, so it is literally an alias of web_fetch_exa. And agent_run is passed a callWindowMs computed by resolveAgentCallWindowMs, which is how the maximum-duration environment variables take effect.

After the tools, the function registers MCP prompts and resources, which most people never look at but which are part of the server’s contract:

  • A prompt named web_search_help, described as “Get help with web search using Exa”, whose single user message asks the assistant to help search for recent news about artificial intelligence breakthroughs. It is a starter prompt, useful mainly to prove the wiring in a client that lists prompts.
  • A resource exa://tools/list (JSON) that returns the metadata of every tool in the registry, each with an enabled flag that reflects whether the tool is actually registered in this session. This is a convenient way for an agent, or you, to check what a given URL or config really produced.
  • If any agent tool was registered, an agent_research_help prompt that takes an optional task argument and returns two messages: your task (or a request to help structure one) and an embedded resource holding the Exa Agent research guide; plus two resources, exa://agent/skill (the guide as markdown) and exa://agent/schema-templates (starter JSON Schema templates for common agent workflows).

Finally the function attaches analytics tracking from the agnost package. The call passes a configuration that disables logs, input, output and error capture (disableLogs, disableInput, disableOutput, disableError are all true), and it is wrapped in a try/catch so a failure to set up analytics is logged in debug mode and otherwise ignored. If you review servers for privacy, this is a line you will want to read for yourself; the point here is only that the dependency exists and how it is configured.

The tool registry

The registry lives in src/toolRegistry.ts and is the single source of truth for tool IDs, human-readable names, descriptions, default-enabled flags, groupings and the key requirement. It is a plain object typed with satisfies Record<string, ToolMetadata>, so adding a tool without the required fields fails type checking. Here is what it says today, in a form you can rely on when reasoning about a config:

Tool ID Enabled by default Group Needs your own key Status
web_search_exa yes search no current
web_fetch_exa yes search no current
web_search_advanced_exa no search no current, opt-in
agent_run no agent yes current, opt-in
get_code_context_exa no search no deprecated, use web_search_exa
company_research_exa no search no deprecated, use web_search_advanced_exa
people_search_exa no search no deprecated, use web_search_advanced_exa
linkedin_search_exa no search no deprecated, use web_search_advanced_exa
crawling_exa no search no deprecated, use web_fetch_exa
deep_search_exa no search yes deprecated, use web_search_advanced_exa
deep_researcher_start no search yes deprecated
deep_researcher_check no search yes deprecated

The design decision embedded in this table is worth appreciating. The default set is deliberately small: two tools, both read-only, both cheap to reason about. Everything that has a larger surface or a larger cost profile is opt-in. For an agent, a short tool list is a feature: every extra tool description is context the model has to read on every turn, and every extra tool is another decision the model can get wrong. The registry makes the small default the path of least resistance.

Anatomy of a tool call

Take the most common call, web_search_exa, and follow it from the client to the network and back. The file is src/tools/webSearch.ts.

Registration. registerWebSearchTool(server, config, toolName) calls server.tool(...) with a name (defaulting to web_search_exa), a long description written for the model, a zod schema for parameters, an annotations object, and an async handler. The annotations are readOnlyHint: true, destructiveHint: false, openWorldHint: true and idempotentHint: true. These are hints that MCP clients can use to decide how much friction to put in front of a call: a read-only, non-destructive, idempotent tool is a good candidate for auto-approval, while openWorldHint: true is the server’s honest statement that the tool touches the open internet.

Argument validation. Parameters are validated with zod, but with deliberately forgiving helpers from src/tools/validation.ts. lenientString() accepts a string and also coerces accidental numbers and booleans into strings before requiring a non-empty trimmed string. lenientOptionalNumber() coerces numeric strings to numbers and, on anything invalid, quietly becomes undefined instead of throwing. lenientOptionalPositiveNumber() does the same with a minimum of 1. lenientOptionalBoolean() accepts true, 1 and yes (in any case) as true, false, 0 and no as false, and otherwise falls back to undefined. The reason for this leniency is practical: language models sometimes emit "5" where a number belongs, or "true" where a boolean belongs, and a strict schema turns those small slips into failed tool calls and wasted turns. The lenient helpers absorb them.

Query preprocessing. The handler looks for a category: token in the query using a case-insensitive pattern that accepts company, publication, news, personal site and people. If it finds one, it removes the token from the query text, collapses whitespace, and passes the category to the search API as a structured filter instead. This is a small piece of API design with big consequences for how you write queries, and the Features section returns to it.

Request construction. The handler builds an ExaSearchRequest with the cleaned query, a search type (the configured default, or auto), numResults (default 10) and a contents object requesting highlights. Highlights are short, query-relevant excerpts, so the default response is compact.

Client and headers. createExaClient(config) in src/tools/config.ts builds an Exa client from the exa-js package. If the config carries an OAuth access token, the client is created with a placeholder key and the x-api-key header is deleted so that the bearer token is used instead. Otherwise the key comes from the config (exaApiKey) or from process.env.EXA_API_KEY, falling back to an empty string. The integrationHeaders helper then adds an x-exa-integration header naming the tool (for example web-search-mcp), optionally suffixed with a percent-encoded source label, plus Authorization: Bearer ... when an OAuth token exists, an x-exa-mcp-session-id header when a session ID exists, and an x-exa-mcp-client header carrying serialized metadata about the MCP client. Those headers exist so Exa can attribute traffic to the MCP integration and to a client; they are a useful thing to know about when you write egress rules or review what leaves your machine.

Network call with retry and timeout. The request goes to the Exa API’s /search endpoint with POST, wrapped in two helpers from src/utils/errorHandler.ts. retryWithBackoff retries only on transient Exa errors, defined as HTTP 500, 502, 503 and 504, up to two retries with exponential backoff starting at one second (so waits of roughly one and then two seconds). withTimeout races the whole operation against a timer and rejects with a message like web_search_exa timed out after 60s. The configured timeouts are 60 seconds for search, 60 seconds for fetch and 300 seconds for advanced search.

Sanitization. Before anything is formatted, the response goes through sanitizeSearchResponse. This is a whitelist, not a blacklist: the sanitizer constructs a fresh object containing only the fields it recognizes and drops the rest, and it separately strips any key named requestTags from nested objects. The Advanced topics section describes exactly what survives.

Formatting. For web_search_exa the handler renders each result as a block of lines: Title:, URL:, Published:, Author: (each with N/A when missing), followed by Highlights: and the highlight strings if any exist, or a Text: line if only text exists. Blocks are joined with a --- separator. The tool’s response carries this as a single text content item, with the search time attached as _meta.searchTime. If the API returns no results, the tool returns a plain sentence, “No search results found. Please try a different query.”, rather than an error, which is the right behavior: an empty result is information, not a failure.

Error mapping. Anything thrown is passed through formatToolError, which returns an MCP tool result with isError: true. Its logic has three branches: if the error is an HTTP 429 and the session is using the free, keyless path, it returns a specific instruction to create an API key and update the URL; if the error is an ExaError, it returns <tool> error (<status>): <message> with a timestamp line when the API provided one; anything else becomes <tool> error: <message>. Returning errors as tool results instead of throwing protocol-level errors is what lets the model read the message and react, for example by telling you to add a key.

Authentication and rate limiting

Exa MCP supports three authentication modes, and understanding them removes most setup confusion.

Anonymous. The README says the hosted server “works anonymously with rate limits.” There is no key and no sign-in. This is the fastest way to try the server, and it is what you get if you paste the bare URL into a client.

OAuth. The README calls OAuth “preferred”: “most clients prompt you to sign in to Exa.” The verification code in src/utils/auth.ts shows how it works on the server side. It uses the jose library to verify a JWT access token against a remote JSON Web Key Set at https://auth.exa.ai/api/oauth/jwks, requiring the issuer https://auth.exa.ai and the audience https://mcp.exa.ai (both overridable through environment variables for self-hosted setups). A valid token must carry an exa:team_id claim; an exa:api_key_id claim and a scope are optional. A helper checks whether a string looks like a JWT (three dot-separated base64url segments), which lets the server distinguish a bearer JWT from a plain API key sent in the same header. When OAuth is in use, createExaClient sends the token upstream as a bearer credential and removes the key header. The practical upside for you is that no long-lived secret lives in your config file.

API key. If you prefer a key from the Exa dashboard, the README lists three ways to send it to the hosted server: the URL parameter ?exaApiKey=..., an Authorization: Bearer ... header, or an x-api-key header. For the local stdio server, you set EXA_API_KEY in the environment. A note on hygiene: a key in a URL query string is easy to paste and easy to leak into logs, browser history, screenshots and shared config files. If your client supports headers, prefer that. If it supports OAuth, prefer that even more.

Rate limiting for anonymous users. The hosted handler contains an IP-based limiter for keyless traffic. The source comments state the design: “Users who provide their own API key via ?exaApiKey= bypass rate limiting,” and limiting “only applies to actual tool calls (tools/call method), not to basic MCP protocol methods like tools/list, initialize, ping, etc.” The limiter is built on @upstash/ratelimit with a sliding window and is lazily initialized only when Redis credentials are configured, so a self-hosted runtime without them simply has no limiter. The configurable knobs are RATE_LIMIT_QPS, documented with a default of 2, and RATE_LIMIT_DAILY, documented with a default of 50. Treat those numbers as the repository’s documented defaults, not as a promise about what Exa’s production deployment enforces today; if you depend on a specific quota, ask Exa or supply your own key.

When the limit is hit, the model receives a message that begins “You’ve hit Exa’s free MCP rate limit” and tells it exactly what to do: create a key at the dashboard and change the MCP URL to include ?exaApiKey=YOUR_EXA_API_KEY. That message is a nice example of writing errors for a model as the reader. It is specific, it names the fix, and it does not leave the agent guessing.

Agent skills and plugins

The last architectural piece is not code that runs in the server at all. The repository is packaged as an “Agent Plugin” (it has .claude-plugin/plugin.json and marketplace.json, a top-level plugin.json, a gemini-extension.json and an mcp.json), and it ships two skills under skills/:

  • search: an orchestration guide for deep research, lead generation, literature reviews, competitive analysis and “any query where one search falls short”. It tells the assistant to classify query complexity, dispatch parallel sub-tasks, deduplicate and synthesize. It comes with reference files for searching, filtering, extraction, synthesis and source quality, plus pattern files for people, companies, papers, relationships, code and news.
  • exa-agent: a guide to running Exa Agent through agent_run, including how to define an objective and output schema, how to continue runs, and when to write a batch script instead.

Skills are instructions for the client’s assistant, not features of the server. That distinction matters when you troubleshoot. If you install only the MCP server, you get tools. If you install the plugin, you also get the skills, which shape how the assistant uses those tools. Later sections quote from the skills because they are the most authoritative description of how Exa expects its own tools to be used.

Features

This section is a reference for every tool the current source defines. For each one it gives the purpose (from the tool’s own description, which is what the model reads), the parameters, the behavior you can rely on, the output shape, and the mistakes people make. Parameter names and defaults are quoted from the source at v3.4.1.

web_search_exa

web_search_exa is the workhorse and one of the two tools you get by default. Its description, which is the text that shows up in the model’s tool list, reads: “Search the web for any topic and get clean, ready-to-use content.” It then adds three pieces of guidance. Best for: “Finding current information, news, facts, people, companies, or answering questions about any topic.” Returns: “Clean text content from top search results.” And a short block of query tips that is the single most important thing to internalize about how this tool behaves: “describe the ideal page, not keywords. ‘blog post comparing React and Vue performance’ not ‘React vs Vue’.”

Parameters

Parameter Type Required Default Notes
query string yes none A natural-language description of the ideal page. May contain one inline category:<type> token. Numbers and booleans are coerced to strings.
numResults number no 10 Number of results to return. Numeric strings are accepted; invalid values fall back to the default.

There are only two parameters, and that is intentional. The tool is designed to be hard to misuse. Everything else (search type, highlights) is fixed by the server: the search type is auto unless the deployment sets DEFAULT_SEARCH_TYPE to fast or instant, and content is requested as highlights.

The category token

The basic search tool has no category parameter, but it supports the same idea through the query string. If the query contains category:company, category:publication, category:news, category:personal site or category:people (case-insensitive), the handler extracts the token, removes it from the query text, normalizes whitespace, and sends the category as a structured filter to the search API. Everything else in the query is used as the semantic search text.

Some examples, taken from the tool’s own schema description and Exa’s search guidance:

{ "query": "category:people John Doe software engineer" }
{ "query": "category:company developer tools for API testing", "numResults": 10 }
{ "query": "category:publication sparse attention mechanisms for long context", "numResults": 10 }
{ "query": "category:news engineer launches startup", "numResults": 15 }

Three behaviors follow from the implementation and are easy to trip over. First, only the five listed categories are recognized inline. A token like category:github or category:pdf is not extracted and would be sent as ordinary query text; those categories exist only on the advanced tool. Second, the pattern uses word boundaries, so it works anywhere in the string, but it removes only the first match; two category tokens in one query would leave the second as plain text. Third, because the token is stripped from the text, you should not write the category words as part of the sentence you want embedded. Write category:company AI infrastructure startups, not category:company companies that are companies building AI infrastructure.

Search types

Exa’s search endpoint supports multiple search types, and the source recognizes three: auto, fast and instant. The advanced tool’s schema describes them: auto is “high quality and works with all filters (recommended)”, fast gives “quick results”, and instant gives “fastest results”. The basic tool does not expose the choice per call; a deployment picks one through DEFAULT_SEARCH_TYPE for the stdio server (which populates the defaultSearchType config field) and otherwise uses auto. If you run the stdio server yourself and your workload is latency-sensitive, this is the knob to turn. If you use the hosted endpoint, you get the hosted default.

What comes back

The tool returns a single text content item. Each result is a block of lines, and blocks are separated by a line containing ---. The shape, with placeholders, is:

Title: <page title or N/A>
URL: <page url>
Published: <published date or N/A>
Author: <author or N/A>
Highlights:
<one or more highlight excerpts>

---

Title: <next result>
...

If a result has no highlights but does have text, a Text: line appears instead of the Highlights: block. The response also carries the server-measured search time in the content item’s _meta.searchTime, which clients may or may not surface. If the search returns nothing, the model is told: “No search results found. Please try a different query.”

Because the default content mode is highlights, the result is intentionally partial. The tool description says so directly: “If highlights are insufficient, follow up with web_fetch_exa on the best URLs.” Treat web_search_exa as the discovery step and web_fetch_exa as the reading step. Agents that try to answer detailed questions from highlights alone tend to produce shallow answers; agents that search, pick two or three promising URLs, and fetch them tend to produce better ones. That is not a benchmark, it is simply the workflow the tool descriptions and the bundled skills prescribe.

Writing queries that work with a semantic index

Exa’s own guidance, in skills/search/references/searching.md, explains the mental model in three sentences: “Exa uses vector embeddings, not keywords. It finds pages semantically similar to your query. It does not match keywords exactly, directly understand boolean logic (AND/OR/NOT), or validate that results meet your criteria.” From that, the guide derives practical rules.

Describe the page, not the fact. The rewrite table in the skill is a good template:

You are looking for Weak query Stronger query
Blog posts about a topic X detailed blog post about X written by a practitioner
A company that does something Y company category:company startup building Y for enterprise
A person in a role person at company category:people senior engineer at Acme

Write natural grammatical phrases. Fragments and keyword lists produce scattered results. The skill lists “very short queries (1-2 words)” as an anti-pattern.

Do not use boolean operators or quotes. “Boolean operators (‘AND’, ‘NOT’) are just words to Exa, not operators” and “quotes don’t force exact phrase matching.” If you need exclusion or exact-phrase behavior, that is what the advanced tool’s includeText, excludeText, includeDomains and excludeDomains parameters are for.

Size numResults to the precision of the query. The guidance is a small decision table:

Query type Suggested numResults Example intent
Named entity (one specific person or company) 5 a specific startup’s founding story
Precise filter (narrow category plus constraints) 10 Series A developer-tools companies in a niche
Broad discovery (wide category, few constraints) 15 news about engineers launching startups

And a hard ceiling: “Never use numResults above 25. If you need more coverage, run more queries with different angles at n=10-15 rather than one query at n=50.” The reasoning is sound for any embedding search. The tail of a long result list is mostly noise, and every result you ask for is text you pay for in context.

Diversify by angle, not by synonym. If you run several searches on one topic, the skill says to vary the perspective, not the vocabulary. “Overhyped”, “overrated” and “disappointment” land in the same region of embedding space, so three searches with those words are one search with extra cost. A skeptic’s angle, a builder’s angle and a practitioner’s angle are different searches. The skill also notes that word order can change results (“Python async patterns for web scraping” versus “web scraping async patterns in Python”), which is a cheap way to widen coverage when you need it.

Encode time in words, and compute dates yourself. For questions about “last week” or “recent”, the skill’s instruction is to calculate exact dates from the current date in the environment and to encode them semantically in the query, for example “published in March 2026”, rather than trusting a model’s sense of today’s date. When you need a hard date boundary, use the advanced tool’s startPublishedDate and endPublishedDate.

When nothing comes back, lengthen and sharpen the query, try a different angle rather than a synonym, and if several angles fail, report that the topic has limited web coverage instead of inventing results. That last clause is worth repeating to any agent you wire up, because “the search found nothing” is a legitimate answer that models are tempted to paper over.

Approval and safety annotations

The tool declares readOnlyHint: true, destructiveHint: false, idempotentHint: true and openWorldHint: true. Clients differ in how they use these hints. A cautious client policy is to auto-approve read-only, non-destructive tools while still surfacing the fact that they reach the open web. Remember that hints are hints: a client decides how much to trust them, and a malicious server could lie. For a first-party server you can inspect (this one is MIT-licensed and public), the hints match the code.

web_fetch_exa

web_fetch_exa reads pages. Its description: “Read a webpage’s full content as clean markdown. Use after web_search_exa when highlights are insufficient or to read any URL.” Then: “Best for: Extracting full content from known URLs. Batch multiple URLs in one call. Returns: Clean text content and metadata from the page(s).”

Parameters

Parameter Type Required Default Notes
urls array of strings yes none URLs to read. A JSON-encoded array in a string is parsed; a bare string becomes a one-element array.
maxCharacters number (min 1) no 3000 Maximum characters extracted per page.

The permissive handling of urls is another example of designing for model behavior. Models occasionally pass a JSON array as a string, or a single URL as a bare string, and the schema’s preprocessing step accepts both without failing the call.

The default limit, and a discrepancy worth knowing about

Read this part carefully, because it changes how much text you get. In the source, the request sent to the Exa contents endpoint is { urls, text: { maxCharacters: maxCharacters || 3000 } }. The constant is API_CONFIG.DEFAULT_MAX_CHARACTERS, equal to 3,000. So if you omit maxCharacters, each page is capped at 3,000 characters, which is roughly 500 words. That is enough for a summary of a short page and not nearly enough for a long article, a documentation page or a specification.

Meanwhile, the bundled extraction guidance in skills/search/references/extraction.md says: “Avoid using maxCharacters param or head/tail bash tools; the point is to understand full page context.” Those two facts sit uncomfortably together. The guidance’s intent is clear (do not truncate the pages you read), but the code’s default truncates at 3,000 characters when the parameter is omitted. The safe interpretation for your own agents: if you want whole pages, pass a generous maxCharacters explicitly, and check the output length against what you expect. Do not assume that omitting the parameter means “everything”. If you run the stdio server, you can also verify the behavior directly by fetching a long page and counting.

What comes back

The handler formats each successful result as a small markdown document: a heading line # <title or (no title)>, a URL: line, optional Published: (date part only) and Author: lines, a blank line, then the page text. Failures are appended as lines of the form Error fetching <url>: <tag> using the tag Exa returns for that URL (for example a status tag). The three outcomes are distinguished carefully:

  • Some results, some failures: successes are formatted first, then one error line per failed URL, all in a normal (non-error) response. Partial success is not treated as an error, which is right for a batch.
  • No results, at least one failure: the response is marked isError: true with a message like Error fetching URL(s): <url>: <tag>; ....
  • No results and no failures: a normal response reading “No content found for the provided URL(s).”

Just as with search, the raw API response is passed through a whitelist sanitizer before formatting. And the same retry and timeout wrapping applies: transient upstream errors retried up to two times with exponential backoff, and a 60-second timeout for the whole operation.

Batching and habits

The extraction guidance says to “batch up to 5-10 URLs per fetch call to minimize round trips.” That matches the tool description (“Batch multiple URLs in one call”). One call with eight URLs is one tool invocation for the model to reason about, versus eight. The trade-off is context: eight pages at the default cap is about 24,000 characters; eight pages with a large maxCharacters can be far more. Size the batch to what you intend to do with the text.

A last habit from the search skill: when web_fetch_exa fails on a particular URL, “fall back to fetching with any other fetch tool you have access to. If that also fails, skip it and work with remaining sources.” That advice will make more sense in the Comparison section, where the general-purpose fetch server and browser automation servers come up as complements.

web_search_advanced_exa

The advanced tool is off by default and is the one you enable when you know exactly what you need. Its description is candid about who it is for: “Advanced web search with full control over filters, domains, dates, and content options. Best for: When you need specific filters like date ranges, domain restrictions, or category filters. Not recommended for: Simple searches - use web_search_exa instead. Returns: Search results with optional highlights, summaries, and subpage content.”

Enable it by URL (https://mcp.exa.ai/mcp?tools=web_search_advanced_exa) or, for stdio, with ENABLED_TOOLS=web_search_exa,web_fetch_exa,web_search_advanced_exa. Remember that naming tools replaces the defaults. If you write only web_search_advanced_exa, you will have only that tool.

Parameters, grouped

Query and shape

Parameter Type Notes
query string “can be a question, statement, or keywords”.
numResults number 1 to 100; default 10.
type auto | fast | instant Default auto.
category enum company, publication, news, pdf, github, personal site, people, financial report.

Where results may come from

Parameter Type Notes
includeDomains string[] Only these domains, for example ["arxiv.org", "github.com"].
excludeDomains string[] Never these domains.
userLocation string ISO country code such as US, GB, DE, for geo-targeted results.
moderation boolean Filters out unsafe or inappropriate content.

When results were published or crawled

Parameter Type Notes
startPublishedDate string ISO 8601 YYYY-MM-DD; results published after this date.
endPublishedDate string Results published before this date.
startCrawlDate string Results crawled after this date.
endCrawlDate string Results crawled before this date.

What the text must contain

Parameter Type Notes
includeText string[] Only results containing all of these strings.
excludeText string[] Exclude results containing any of these strings.
additionalQueries string[] Extra query variations to widen coverage.

What content comes back per result

Parameter Type Notes
textMaxCharacters number (min 1) Cap on text extracted per result. If unset, full text is requested (text: true).
contextMaxCharacters number (min 1) Cap for a combined “context” string, which is not included by default.
enableSummary boolean Ask for a generated summary per result.
summaryQuery string Focus query for the summary.
enableHighlights boolean Ask for highlight excerpts.
highlightsMaxCharacters number Total characters across highlights per URL. Preferred over sentence counts.
highlightsQuery string What the highlights should be relevant to.
highlightsNumSentences number Deprecated; mapped to roughly 1,333 characters per sentence.
highlightsPerUrl number Deprecated; currently ignored server-side.

Freshness and crawling depth

Parameter Type Notes
maxAgeHours number Maximum age of cached content. 0 always fetches fresh content; omitting it uses cached content with a fresh fetch as fallback.
livecrawlTimeout number Milliseconds allowed for a fresh fetch when maxAgeHours triggers one.
subpages number How many subpages to crawl from each result (1 to 10).
subpageTarget string[] Keywords that steer which subpages are selected.

A detail visible in the handler: when you do not set maxAgeHours, the request carries livecrawl: "fallback", which asks Exa to use cached content and fetch live only when needed. When you do set maxAgeHours, that value takes precedence instead. So “always fresh” is maxAgeHours: 0, and “whatever is fastest” is leaving it unset.

Three illustrative calls

These are examples of the argument shape, not recorded outputs.

A dated, domain-restricted literature search:

{
  "query": "peer-reviewed study on retrieval-augmented generation evaluation methods",
  "category": "publication",
  "includeDomains": ["arxiv.org"],
  "startPublishedDate": "2025-01-01",
  "numResults": 15,
  "enableHighlights": true,
  "highlightsMaxCharacters": 1500,
  "highlightsQuery": "evaluation metrics and benchmarks"
}

A source-code lookup limited to repositories:

{
  "query": "example of a TypeScript MCP server registering a tool with zod input validation",
  "category": "github",
  "numResults": 8,
  "textMaxCharacters": 2500
}

A fresh company-page crawl with subpages:

{
  "query": "pricing and plans for a developer-focused observability company",
  "category": "company",
  "maxAgeHours": 0,
  "subpages": 3,
  "subpageTarget": ["pricing", "plans"],
  "enableSummary": true,
  "summaryQuery": "what does each plan include and cost"
}

The response

Unlike the basic tool, the advanced tool does not render a friendly text block. It returns JSON.stringify(sanitized), a single compact JSON string, in one text content item, with the same _meta.searchTime. The keys that survive sanitization are described in the Advanced topics section; at the level of a working agent, expect a top-level object containing results (each with fields such as title, url, publishedDate, author, text, summary, highlights, score, and possibly subpages), plus metadata like requestId, resolvedSearchType, searchTime and, if the API provided it, costDollars. The 300-second timeout, five times the basic tool’s, acknowledges that subpage crawling and fresh fetches can be slow.

One annotation detail deserves a mention. The advanced tool declares openWorldHint: false, while web_search_exa and web_fetch_exa declare true, even though all three reach out to the public web through Exa. If your approval policy keys off that hint, the advanced tool will look “closed world” to it. A safer policy is to key off the tool’s identity and your own risk assessment, and not to lean on that single flag.

agent_run

agent_run is the most capable and most different tool in the set. Its registry description: “Run an Exa Agent for multi-step research, list-building, enrichment, or structured output. Returns the final output or a run ID for retained-run continuation.” It is disabled by default and requires authentication: the README says “Exa Agent requires authentication (OAuth or an API key)”, and the server’s registration logic refuses to register the tool for a session that has not supplied its own credentials.

The mental model is different from search. A search call is one lookup. An agent run hands a goal to an Exa-operated research agent that plans, searches, reads and assembles a structured answer over many steps, and this tool is the MCP-facing wrapper that starts the run, streams progress, and lets you resume it.

Parameters

Parameter Type Notes
query string “Natural-language research or enrichment objective. Provide query or runId, not both.”
runId string starting agent_run_ Check on or keep waiting for a run started earlier. “Do not start a duplicate run.”
systemPrompt string Optional system-level guidance for the agent.
outputSchema object Optional JSON Schema for the output. “Prefer a top-level object with bounded arrays and source/evidence fields.”
input.data array of objects Known rows or entities to enrich or process.
input.exclusion array of objects Entities the agent should avoid returning again.
dataSources array (max 5) of { provider } Optional Exa Connect providers for this run.
previousRunId string starting agent_run_ A completed earlier run to use as context for a new one.
effort minimal | low | medium | high | xhigh | auto Defaults to low.

Exa Connect providers

The dataSources parameter accepts these provider names, and the exa-agent skill describes what each is for:

Provider What it adds
fiber B2B company, people, jobs and contact enrichment
financial_datasets Ticker-based news for US public companies
similarweb Website traffic estimates, rankings and competitor discovery
baselayer US business verification, officers, registrations and KYB
affiliate Product catalog search, pricing, brands and merchant links
particle Podcast transcript search with speaker attribution and timestamps
jinko Travel destination discovery ranked by fare

The skill adds a caution: “Do not suggest request-only providers unless the user explicitly says their Exa account already has them enabled.” And a tip: “Name the provider-specific data you want in both the query and the schema so Agent uses the provider instead of falling back to web search.”

How a run behaves inside one MCP call

MCP tool calls are request and response, but a research run can take a long time. The source handles that tension with a call window. By default, the maximum function duration is 800 seconds and 50 seconds of headroom is reserved, so agent_run will wait up to 750 seconds inside a single call. It emits a heartbeat every 15 seconds and polls run state every 4 seconds when it has to poll. If the run finishes inside the window, you get the final output in the same call. If it does not, the tool returns a status of running along with a run ID, and you call agent_run again with runId set until the output is ready. The skill spells this out: “If the response has status: 'running', call agent_run again with that runId until outputReady is true.”

The progress plumbing is defensive. Progress messages are sent through a small emitter that gives the client two seconds to accept each notification; if a delivery does not complete in that time, the emitter disables itself for the rest of the call rather than blocking the run. If the client aborts the call, the server attempts to cancel the upstream run. The skill notes the consequence: if you realize the objective or schema is wrong, abort the call and start a corrected run.

One special case is documented for teams with Zero Data Retention. New runs always stream, output is only available on that live stream, and it cannot be picked up later through runId or previousRunId. If a ZDR run cannot finish in about 750 seconds, the guidance is to retry with lower effort or split the task.

Designing the objective and the schema

The exa-agent skill is essentially a checklist for good runs. Before creating one, write down the objective, the universe of entities that qualify, the segments (geographies, industries, personas, dates), the coverage target, the output fields, the evidence requirements and the exclusions. Then follow its schema rules:

  • Use a top-level object, with list rows in a named array field.
  • Put maxItems on arrays when possible.
  • Include source and evidence fields, not just conclusions.
  • Include stable identifiers such as company name, website or domain, LinkedIn URL, ticker or CIK.
  • Include confidence or rationale fields for fuzzy judgments.
  • Keep required fields to what must exist.
  • Use format: "uri", "email" or "phone" where appropriate.

Here is the skill’s example schema for a company list, shortened for readability:

{
  "type": "object",
  "properties": {
    "companies": {
      "type": "array",
      "maxItems": 50,
      "items": {
        "type": "object",
        "properties": {
          "company_name": { "type": "string" },
          "website": { "type": "string", "format": "uri" },
          "segment": { "type": "string" },
          "why_it_qualifies": { "type": "string" },
          "evidence_url": { "type": "string", "format": "uri" },
          "confidence": { "type": "string", "enum": ["low", "medium", "high"] }
        },
        "required": ["company_name", "website", "why_it_qualifies", "evidence_url"]
      }
    },
    "coverage_notes": { "type": "string" },
    "known_gaps": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["companies", "coverage_notes"]
}

The presence of coverage_notes and known_gaps in that schema is not decoration. It reflects the skill’s insistence on honesty about completeness, covered in the Use cases section.

Deprecated tools and how to migrate

The registry marks eight tools as deprecated. They still exist in code, they are disabled by default, and you can re-enable them by naming them in ?tools= or ENABLED_TOOLS, which is what keeps old configs working. But new work should not use them. Here is the migration map, with what each old tool did according to the source.

Deprecated tool Use instead What it did
get_code_context_exa web_search_exa Searched for code examples and documentation with type: "fast", highlights focused on the query, and 300 characters of text per result.
company_research_exa web_search_advanced_exa Took a companyName, searched "<name> company" in the company category, defaulting to 3 results with highlights.
crawling_exa web_fetch_exa An alias: the same fetch implementation registered under the older name.
people_search_exa web_search_advanced_exa Searched for people and professional profiles.
linkedin_search_exa web_search_advanced_exa LinkedIn-oriented search.
deep_search_exa web_search_advanced_exa Deep search with query expansion and synthesized answers; required your own key.
deep_researcher_start agent_run Started a long research task; required your own key.
deep_researcher_check agent_run Checked a research task’s status and retrieved results; required your own key.

A small inconsistency inside get_code_context_exa illustrates why relying on parameter descriptions alone is risky: its numResults description says “default: 8”, while the code falls back to the shared default of 10. If you keep an old integration alive, test what it actually does.

If your agents were written against the four-tool listing you may have seen on a catalogue page, the practical migration is short. Replace calls to get_code_context_exa with web_search_exa phrased as a code-oriented description of the page you want, or with web_search_advanced_exa and category: "github" when you want repositories. Replace company_research_exa with web_search_exa using category:company, or the advanced tool if you need filters. Replace crawling_exa with web_fetch_exa and set maxCharacters deliberately. Then remove the old names from any explicit tool list so the defaults, or your new explicit list, apply.

Integration

This section is about getting Exa MCP working in the places you actually work: coding agents, desktop assistants, your own code, and your own infrastructure. It also covers how credentials are resolved, because most “it doesn’t work” reports come down to which key or token the server saw.

Choosing an integration path

There are five practical ways to connect, and the right one depends on who you are and what you can install.

Path Best for What you configure Notes
Hosted URL, anonymous Trying it in two minutes The URL https://mcp.exa.ai/mcp Rate limited by IP. No secrets involved.
Hosted URL with OAuth Individuals and teams using OAuth-capable clients The same URL; the client prompts you to sign in Preferred by the README. No long-lived secret in your config.
Hosted URL with an API key Automation, CI, clients without OAuth The URL plus x-api-key or Authorization: Bearer header (or ?exaApiKey=) Lifts anonymous rate limits; required for agent_run unless you use OAuth.
Local stdio via npm Offline-first setups, GUI clients that only launch local commands npx -y exa-mcp-server and EXA_API_KEY Needs Node 20 or newer.
Self-hosted HTTP runtime Organizations that must run the server inside their own network A container built from the repository’s Dockerfile You own rate limiting, TLS and access control.

If you have no strong constraint, start with the hosted URL and OAuth. It is the path the README leads with, and it keeps secrets out of files.

How credentials are resolved

Before the client-specific recipes, it helps to know exactly how the hosted handler decides who you are, because the rules are precise and explain several confusing behaviors. The logic is in getConfigFromRequest in api/mcp.ts. The documented priority is:

  1. An x-api-key header. If present, its value is used as the Exa API key and the session counts as user-provided.
  2. An Authorization: Bearer <token> header. If the token looks like a JWT (three base64url segments), the server verifies it as an OAuth access token against Exa’s authorization server. If verification succeeds, the session is an OAuth session. If verification fails, the request is answered with HTTP 401 rather than being silently downgraded. If the token does not look like a JWT, it is treated as a plain API key.
  3. A ?exaApiKey= query parameter, considered only when neither header above is present. The source labels this “backwards compatible.”
  4. The EXA_API_KEY environment variable of the server process, which is the path the handler itself labels free_tier.

Two consequences are worth remembering. Headers beat URLs. If your client sends both a header and a URL parameter, the header wins, which is what you want if you are rotating keys. And an expired OAuth token produces a 401 with a challenge, not a silent fallback to the free tier. The comment in the code explains the reason: falling through to anonymous access “would mask the expired-credential signal and prevent the client’s refresh flow from triggering.” If you have ever seen a client repeatedly ask you to sign in again, that mechanism is the second half of the story: the server is deliberately telling the client the token is bad.

The same handler also reads a few other URL parameters, all optional:

Parameter Effect
tools Comma-separated tool IDs; replaces the default tool set.
debug true enables debug logging for the request.
defaultSearchType auto, fast or instant; sets the default for web_search_exa.
agentCallWindowMs Sets how long agent_run waits inside one call, capped by the maximum duration.
login Empty, 1, true or yes forces the OAuth login flow.
client A free-form client label; values containing plugin force OAuth.

One architectural note explains why per-request URL configuration is safe. The handler’s header comment says it creates “a fresh handler for each request” so that “each request gets its own configuration (no API key leakage between users).” That is an important property for a multi-tenant server, and it is the reason the tool set and credentials can live in the URL and headers instead of in server-side sessions.

Claude Code

Claude Code can add a remote MCP server from the command line. The general form is claude mcp add --transport http <name> <url>, so for Exa:

claude mcp add --transport http exa https://mcp.exa.ai/mcp

Once added, the server’s tools appear to the model namespaced by server, as mcp__exa__web_search_exa and mcp__exa__web_fetch_exa, and the /mcp command inside Claude Code shows connection status and lets OAuth-capable servers start their sign-in flow. If you would rather pass a key as a header, the same command accepts a header option:

claude mcp add --transport http exa https://mcp.exa.ai/mcp \
  --header "x-api-key: YOUR_EXA_API_KEY"

To share the setup with a team without committing a secret, use a project-scoped .mcp.json that reads the key from the environment. Claude Code supports ${VAR} expansion in these files:

{
  "mcpServers": {
    "exa": {
      "type": "http",
      "url": "https://mcp.exa.ai/mcp?tools=web_search_exa,web_fetch_exa,web_search_advanced_exa",
      "headers": {
        "x-api-key": "${EXA_API_KEY}"
      }
    }
  }
}

Each teammate exports EXA_API_KEY in their own shell, and the file is safe to commit.

There is also a first-party plugin route. The README gives the install command:

claude plugin install exa@claude-plugins-official

The repository’s .claude-plugin/plugin.json shows what the plugin configures: an http server pointing at https://mcp.exa.ai/mcp?client=claude-code-plugin with an x-exa-source: claude-code-plugin header. Because that URL carries client=...plugin, the hosted handler forces the OAuth flow for plugin users, which is why the plugin route signs you in instead of running anonymously. The plugin also delivers the search and exa-agent skills, so you get the orchestration guidance along with the tools.

Claude Desktop and other chat clients

Chat clients differ in whether they accept remote MCP URLs directly. Where a client offers a “custom connector” or “remote MCP server” screen, paste https://mcp.exa.ai/mcp and complete the sign-in prompt. Where a client supports only locally launched servers through a JSON file, use the stdio package instead:

{
  "mcpServers": {
    "exa": {
      "command": "npx",
      "args": ["-y", "exa-mcp-server"],
      "env": {
        "EXA_API_KEY": "YOUR_EXA_API_KEY",
        "ENABLED_TOOLS": "web_search_exa,web_fetch_exa,web_search_advanced_exa"
      }
    }
  }
}

GUI applications often start with a different PATH than your terminal, so npx may not be found if Node was installed through a version manager. If the server fails to start, replace "command": "npx" with the absolute path to npx from which npx, and confirm node --version reports 20 or newer, since the package declares "engines": { "node": ">=20.0.0" }.

Cursor

The README’s “Install in Cursor” badge is a deep link whose payload is a base64-encoded configuration. Decoded, it is simply {"url":"https://mcp.exa.ai/mcp"} under the name exa. That tells you exactly what Cursor stores. If you prefer to write the file yourself, add the server to Cursor’s MCP configuration (~/.cursor/mcp.json globally, or .cursor/mcp.json in a project):

{
  "mcpServers": {
    "exa": {
      "url": "https://mcp.exa.ai/mcp"
    }
  }
}

Cursor also accepts a headers object for remote servers, so a key can be supplied as "headers": { "x-api-key": "YOUR_EXA_API_KEY" } if you are not using OAuth.

Visual Studio Code

The README’s VS Code badge encodes {"type":"http","url":"https://mcp.exa.ai/mcp"}. VS Code keeps MCP servers in an mcp.json file, either in the workspace’s .vscode folder or in your user profile, under a servers key. A workspace entry looks like this:

{
  "servers": {
    "exa": {
      "type": "http",
      "url": "https://mcp.exa.ai/mcp"
    }
  }
}

Codex and ChatGPT

The README documents a one-line command for the Codex CLI:

codex mcp add exa --url https://mcp.exa.ai/mcp

and a plugin listing for ChatGPT’s plugin experience. The hosted handler also serves a small .well-known challenge document for OpenAI apps (api/well-known-openai-apps-challenge.ts), which appears to support that listing’s verification.

Gemini CLI

The repository ships a gemini-extension.json that shows the shape Gemini CLI expects for a remote Streamable HTTP server: the key is httpUrl, not url.

{
  "mcpServers": {
    "exa": {
      "httpUrl": "https://mcp.exa.ai/mcp"
    }
  }
}

Put that under mcpServers in ~/.gemini/settings.json (the README’s location for Gemini CLI), or install the extension.

Everything else

The README’s collapsible table lists where other clients keep their configuration. Because each client names its keys slightly differently (some use url, some httpUrl, some serverUrl, some want a type of http and others streamable-http), treat the table as a map to the right file and check that client’s documentation for the exact key.

Client Where to add it (per the README)
Kiro The Kiro power exa-labs/kiro-power-exa, or ~/.kiro/settings/mcp.json
LM Studio An install link, or manually in mcp.json
Replit Replit’s MCP integrations page
Grok Build /marketplace, install Exa, then /mcp to sign in
Gemini CLI ~/.gemini/settings.json
OpenCode opencode.json
Windsurf ~/.codeium/windsurf/mcp_config.json
Google Antigravity mcp_config.json
Zed settings.json under context_servers
Warp Settings, Agents, MCP servers
v0 by Vercel Settings, MCP connections

The generic configuration the README recommends for any client that uses the common mcpServers shape is:

{
  "mcpServers": {
    "exa": {
      "type": "streamable-http",
      "url": "https://mcp.exa.ai/mcp"
    }
  }
}

Choosing a tool set for the job

Because tools replaces the defaults, it is easy to end up with a tool list you did not intend. It helps to think in terms of roles and to write the whole list every time.

Role Tool IDs Hosted URL suffix stdio ENABLED_TOOLS
Default assistant web_search_exa, web_fetch_exa none (the defaults) leave unset
Analyst who needs filters the two above plus web_search_advanced_exa ?tools=web_search_exa,web_fetch_exa,web_search_advanced_exa the same comma list
Research automation the three above plus agent_run ?tools=web_search_exa,web_fetch_exa,web_search_advanced_exa,agent_run the same comma list
Agent-only workflows agent_run ?tools=agent_run (or the alias agent_tools) agent_tools
Minimal reading tool web_fetch_exa ?tools=web_fetch_exa web_fetch_exa

Keep the list short. Every tool description consumes the model’s context on every turn, and a model that must choose among four search-shaped tools will sometimes choose poorly. The registry’s own design (two tools by default) is a good baseline to depart from only when you have a concrete need.

Verifying that the setup works

Do not trust a green “connected” indicator alone. A five-minute verification pass avoids a lot of confusion later.

  1. List the tools. In the client, look at the tools the Exa server provides and compare against the role you intended. A missing agent_run almost always means no credentials; a missing web_search_advanced_exa means the tool list you supplied replaced the defaults or the name is misspelled.
  2. Read the tools resource. The server exposes exa://tools/list, a JSON document listing every tool in the registry with an enabled flag reflecting what this session actually registered. If your client can read MCP resources, this is the definitive answer.
  3. Run a real query. Ask for something that requires the web, such as “search for the latest release notes of a library you use and summarize them,” and watch which tools the client reports invoking.
  4. Test a fetch with a long page. Ask the assistant to read a long documentation page and quote a line from near the end. If it cannot, check maxCharacters, as described in the web_fetch_exa section: the default cap is 3,000 characters per page.
  5. Check the anonymous limit deliberately. If you are running anonymously, know that the free tier is rate limited by IP and that hitting it produces a specific message about creating an API key. Better to see it during setup than in the middle of a task.
  6. Turn on debug logging when something is off. For stdio, set DEBUG=true. For the hosted URL, append debug=true. The handler logs the enabled tool list and the authentication method it resolved (oauth, api_key or free_tier), which is usually enough to see what went wrong.

Programmatic use with the MCP SDKs

Calling Exa MCP from your own code is a good way to build a deterministic pipeline around the tools, or to test them in isolation. The official SDKs support both transports. These examples show the shape of the calls; adapt them to your SDK version.

TypeScript, hosted endpoint:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(
  new URL("https://mcp.exa.ai/mcp?tools=web_search_exa,web_fetch_exa"),
  {
    requestInit: {
      headers: { "x-api-key": process.env.EXA_API_KEY ?? "" },
    },
  },
);

const client = new Client({ name: "exa-demo", version: "1.0.0" });
await client.connect(transport);

const { tools } = await client.listTools();
console.log(tools.map((t) => t.name));

const search = await client.callTool({
  name: "web_search_exa",
  arguments: {
    query: "detailed engineering blog post about designing MCP tool schemas",
    numResults: 5,
  },
});

if (search.isError) {
  throw new Error(JSON.stringify(search.content));
}
console.log(search.content);

await client.close();

TypeScript, local stdio:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: "npx",
  args: ["-y", "exa-mcp-server"],
  env: { ...process.env, EXA_API_KEY: process.env.EXA_API_KEY ?? "" } as Record<string, string>,
});

const client = new Client({ name: "exa-demo", version: "1.0.0" });
await client.connect(transport);
// ...same listTools/callTool calls as above

Python, hosted endpoint:

import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def main():
    url = "https://mcp.exa.ai/mcp"
    async with streamablehttp_client(url) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print([t.name for t in tools.tools])

            result = await session.call_tool(
                "web_fetch_exa",
                {"urls": ["https://example.com/"], "maxCharacters": 8000},
            )
            if result.isError:
                raise RuntimeError(result.content)
            print(result.content)

asyncio.run(main())

Two patterns are worth adopting in any programmatic client. Always check isError. The server returns failures as normal tool results flagged with isError: true, including the rate-limit message and upstream API errors, so a call that “succeeded” at the protocol level can still be a failure at the tool level. And never parse the text output as a stable API. The basic tools return formatted text designed for a model, not for a parser. If your pipeline needs structured fields, use web_search_advanced_exa, which returns JSON, or agent_run with an outputSchema, which is designed for structured output.

Testing with the MCP Inspector

The repository’s package.json defines npm run inspector, which runs npx @modelcontextprotocol/inspector dist/stdio.cjs. That is a convenient way to exercise the tools by hand: you see the schemas the server advertises, fill in arguments in a form, and inspect the raw response, including the _meta.searchTime field. To try the hosted endpoint instead, start the Inspector without a target and connect to https://mcp.exa.ai/mcp using the Streamable HTTP transport. Inspecting a server before wiring it into an agent is a habit worth building for any MCP server, and it is particularly useful here for seeing the difference between the terse tool descriptions the model sees and the full schemas.

Self-hosting the HTTP runtime

Some organizations cannot send search traffic through a third-party MCP endpoint, or want a single internal gateway that every agent in the company uses. The repository supports that with a container image built from its Dockerfile and a small Node HTTP server, src/runtime-server.ts.

Build and run:

git clone https://github.com/exa-labs/exa-mcp-server.git
cd exa-mcp-server
docker build -t exa-mcp-runtime .
docker run --rm -p 8000:8000 exa-mcp-runtime
curl -i http://localhost:8000/ping

The /ping route returns 200 with the body ok, which makes it a natural target for container health checks. Every other request is dispatched to the same GET, POST, DELETE and OPTIONS handlers used by the hosted function, so the behavior you read about in this guide applies to your own instance. Any other HTTP method gets a 405.

Paths. The handler normalizes several paths onto its internal route: /, /mcp, /mcp/oauth, /mcp-oauth and /api/mcp-oauth all map to the MCP endpoint. Point your client at http://your-host:8000/mcp.

Credentials for callers. Read the runtime server’s request translation carefully, because it contains a behavior with security implications. When a request arrives with no x-api-key header, no Authorization header and no exaApiKey query parameter, and the process has EXA_API_KEY set, the runtime injects that key as the request’s x-api-key. In other words, if you run the container with a key in its environment and expose it to callers who present no credentials, those callers will use your key. That is convenient for a private internal gateway and dangerous on a public address. Decide deliberately: either put authentication in front of the container, or do not set a default key and require callers to bring their own.

Rate limiting. The per-IP limiter is only active when Redis-compatible credentials are configured (KV_REST_API_URL and KV_REST_API_TOKEN, or UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN). Without them, the handler logs that rate limiting is disabled, and there is no limiter at all. If you self-host and want limits, configure Redis and the RATE_LIMIT_QPS and RATE_LIMIT_DAILY variables, or enforce limits at your gateway. Note that limits apply only to callers who did not supply their own key.

Environment variables you are likely to set on a self-hosted instance:

Variable Purpose
HOST, PORT Bind address and port. The image sets 0.0.0.0 and 8000.
EXA_API_KEY Default key for callers that present none. See the warning above.
ENABLED_TOOLS Default tool list when a request does not carry ?tools=.
DEFAULT_SEARCH_TYPE Default search type: auto, fast or instant.
DEBUG true for debug logging.
MCP_MAX_DURATION_SECONDS Maximum duration used to derive the agent_run call window.
AGENT_CALL_WINDOW_MS Explicit agent_run window, capped by the maximum duration.
OAUTH_ISSUER, OAUTH_AUDIENCE Where OAuth JWTs are verified. Default to Exa’s issuer and audience.
RATE_LIMIT_QPS, RATE_LIMIT_DAILY Anonymous-caller limits, when Redis is configured.

Behind a reverse proxy. Streamable HTTP can hold a connection open while a long tool call streams progress. If you put a proxy such as nginx in front, disable response buffering for the MCP path and raise read timeouts above the longest call you expect. agent_run‘s default window is about 750 seconds, so a proxy timeout of 60 seconds will cut long runs off. A minimal sketch:

location /mcp {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_buffering off;
    proxy_read_timeout 900s;
}

Also remember the allowed CORS headers the handler advertises (Accept, Content-Type, Authorization, x-api-key, x-exa-source, Mcp-Session-Id, MCP-Protocol-Version, Last-Event-ID), which are what browser-based clients need. Do not strip those at the proxy.

A note on where it runs. The runtime logs an “AgentCore Runtime server” banner at startup and the repository contains a publish-agentcore workflow, which suggests the container is meant to be usable on container-based agent runtimes. The code itself is a plain Node HTTP server, so it runs anywhere Node 22 or Docker runs.

Finding and installing it through the registry

If you are building an agent that discovers servers on its own, the MCP Registry is an agent-native place to do it. The registry is itself an MCP server at https://ai.mcpharbor.dev/mcp (Streamable HTTP, no account), and its plain-text guide for agents lives at ai.mcpharbor.dev/llms.txt. An agent can search for a search server, fetch the entry for io.github.exa-labs/exa-mcp-server, and read its remotes and packages, which is precisely the information used to build the configurations in this section. Humans can do the same in the browser through the MCP server catalogue. Remember the drift caveat from the beginning of this guide: a catalogue entry is a snapshot, so when a listing and the project’s README disagree about tools or versions, trust the README and the source.

Use cases and workflows

Tools are only as useful as the habits around them. This section turns the reference material into concrete workflows. Most of the method comes straight from the guidance Exa bundles with its own server (the search and exa-agent skills), because that guidance encodes how the tool’s authors expect it to be used. The scenarios below are illustrative: the prompts and argument shapes are realistic, but they are examples, not recordings of runs.

The core loop: search, select, read, answer

Almost every good Exa workflow is the same three moves.

  1. Search with web_search_exa to discover candidate pages. Write the query as a description of the ideal page, keep numResults modest, and read the titles and highlights.
  2. Select the two to five results that are most likely to contain what you need. Discard the rest without regret. The skill states the reason bluntly: “Exa returns similarity, not yet validated. A result appearing in search output does not mean it meets the user’s criteria. You must validate.”
  3. Read the selected pages with web_fetch_exa, passing a maxCharacters large enough for the page, then answer from what you read, citing URLs.

An illustrative session, with the assistant’s reasoning summarized:

User: “What changed in the most recent major release of the library we use for schema validation, and does it affect our TypeScript setup?”

Assistant, step 1: web_search_exa with query: "official release notes for the latest major version of zod, breaking changes and TypeScript requirements", numResults: 8.

Assistant, step 2: From the highlights, picks the project’s own changelog page and one migration guide, and drops two blog roundups.

Assistant, step 3: web_fetch_exa with urls: [<changelog url>, <migration guide url>] and maxCharacters: 12000.

Assistant, step 4: Answers with the specific breaking changes, quotes the relevant lines, names the TypeScript version requirement it found, and links both pages.

Three details make this loop robust. The query specifies the type of page (official release notes) rather than only the topic, which steers the semantic search toward primary sources. The selection step is explicit, which prevents the model from summarizing all eight results equally. And the fetch step raises maxCharacters above the 3,000-character default, because release notes are long.

A recommended instruction block for an agent that will use these tools, derived from the tool descriptions and the skills, might read:

When you need current information from the web:
1. Use web_search_exa with a natural-language description of the ideal page.
   Do not use boolean operators or one-word queries. Use numResults 5 for a
   specific entity, 10 for a filtered search, 15 for broad discovery, never above 25.
2. Read titles and highlights. Choose the best few results. Prefer primary
   sources (official docs, filings, the project's own repository) over roundups.
3. Use web_fetch_exa on the chosen URLs, with maxCharacters large enough for the
   page. Batch up to five to ten URLs per call.
4. Cite every factual claim with its URL. If sources disagree, say so.
5. If searches return nothing useful after two different angles, say the topic has
   limited web coverage. Do not invent sources.

Developer research: documentation, errors and examples

Coding agents are the most common users of search MCP servers, and Exa’s tools are shaped for that job. The bundled skill even has a patterns-code.md file for “code, APIs, docs, errors”. The old dedicated tool, get_code_context_exa, has been deprecated in favor of the general tool, so the workflow now uses web_search_exa phrased as code-oriented page descriptions, or the advanced tool with a category filter.

Look up how an API is used. Describe the specific usage, as the deprecated tool’s own examples did: “Python requests library POST with JSON body” rather than “python http”. Adding what kind of page you want helps: “official documentation page showing how to configure middleware in Express” versus a forum thread.

Diagnose an error message. Paste the distinctive part of the error and describe what you want: “GitHub issue or forum thread explaining why this error appears when building a Next.js app with a specific configuration, with the accepted fix”. Then fetch the top one or two threads, because the fix is usually in the answer, not the snippet.

Find real-world examples in repositories. Use web_search_advanced_exa with category: "github" and a description of the pattern, for example an MCP server that registers a tool with schema validation. Set textMaxCharacters to a few thousand so you get enough code to judge relevance.

Check whether a fix is current. Add a date boundary with startPublishedDate on the advanced tool, or encode recency in the query (“published in the last year”), because documentation for fast-moving libraries goes stale quickly. The skill’s guidance on “missing recency” in synthesis makes the same point: do not treat 2023 sources as current for fast-moving topics.

A caution that applies to every developer workflow: search results are untrusted input. A page found by a search can contain instructions aimed at an AI assistant. Treat commands and code from the web as suggestions to review, not as instructions to execute, and never let a search result trigger actions with side effects without the same confirmation you would require for any other model-suggested action. The Security section returns to this.

Company and market research

For questions about companies, the category:company token and the advanced tool’s company and financial report categories are the main levers.

Understand a company. “category:company what does this startup build and who are its customers”, with numResults: 5, gives a handful of company-oriented pages. Then fetch the company’s own site and one or two third-party pages, and compare what the company says with what others say. The source-quality reference in the bundled skill recommends this kind of triangulation: weight independent sources above self-promotion and look for “case studies with specific numbers” rather than adjectives.

Map a landscape. Use several searches with different angles rather than one broad one, following the skill’s decomposition example. For “open-source LLM fine-tuning frameworks for production,” the skill suggests three sub-questions: what production engineers recommend and say about real deployments; what has launched or gained traction recently and is not yet widely known; and what the common complaints, failure modes and reasons for migrating away are. The third angle is the one most people skip, and it is often the most informative.

Find competitors. The skill flags “find competitors to Acme Corp” as an ambiguous request: it could be a quick search for known competitors or a deep sweep across funding databases, press and niche directories. A good agent asks which one you want before spending the tokens. For the deep version, an agent_run with an output schema (below) is the better tool.

Read financial filings. The advanced tool’s financial report category, combined with includeDomains for the relevant regulator or investor-relations sites, narrows results to primary documents. Use web_fetch_exa with a high maxCharacters to read the relevant sections, and remember that a summary of a filing is not the filing.

Finding people and experts

The category:people token and the advanced people category search professional profiles. The workflow is the same as for companies, with two additions.

First, the skill’s source-quality guidance is most valuable here. For expert-finding queries, the primary output should be convergent ideas, not a ranked list of names: “Lead with what the best sources agree on, then cite who said it.” The skill distinguishes practitioners (people who do the work: shipped products, open-source contributions, case studies with specific numbers, “we built X and here’s what happened”) from commentators (roundup posts, “top 10” lists, content that mostly links to others’ work). It lists the noise signals to filter out first: no skin in the game, misaligned incentives (sponsored content, vendor blogs, affiliate-heavy pages), circular credentials, positive-only advice with no tradeoffs, and temporal decay, where someone has shifted from doing to teaching.

Second, use the skill’s verification searches when credibility matters, and only then: who cites this person, what is their track record, and what is the criticism of them. The skill explicitly says to run these only “when the task specifically calls for evaluating source credibility.”

A responsible-use note belongs here. People search returns information about individuals. Use it for legitimate professional purposes such as finding a speaker, a domain expert, a potential hire’s public work or a business contact, follow the terms of the sources involved and applicable privacy law, and do not use it to profile or harass private individuals.

News, events and time-bounded questions

Time is where naive agents fail, and Exa’s guidance is unusually explicit about it. The rules, gathered from searching.md and the main skill:

  • Compute dates yourself. “If the query involves time (‘last week’, ‘recent’, ‘past 6 months’), calculate exact dates from today’s date in your environment context. Write out the calculation explicitly before doing anything else. Never eyeball dates or reuse dates from examples.”
  • Encode time in the query for the basic tool. “Published in March 2026” rather than a date filter, because the basic tool has no date parameters.
  • Use hard filters on the advanced tool when you need a strict window: startPublishedDate and endPublishedDate for when a page was published, startCrawlDate and endCrawlDate for when Exa saw it.
  • Force freshness when it matters. Set maxAgeHours: 0 on the advanced tool to bypass cached content and fetch the live page, and set livecrawlTimeout if you need to bound the wait.
  • Flag ambiguity. The filtering reference says that if a date is ambiguous (“early 2025”), note the uncertainty rather than silently including or excluding.

For “what are people saying about X” questions, use category:news for reporting and category:personal site for practitioner blogs, and synthesize by theme (see the synthesis section below).

Literature review and technical papers

For research papers, combine the publication category (or pdf on the advanced tool) with includeDomains for the repositories you trust, such as preprint servers. A pattern that works well is to run two or three phrasings of the same research question, since “word order affects embeddings” and different phrasings can surface different papers, then merge and deduplicate by URL. Use enableHighlights with a highlightsQuery that states what you are looking for (methods, results, limitations), so the excerpts are relevant to your question rather than the paper’s first paragraph. Fetch only the papers that survive a first pass; abstracts are cheap, full papers are not.

If you need a structured result, such as a table of papers with method, dataset and reported metric, use the extraction discipline from extraction.md: extract literal values from the source, mark fields as “not found” rather than guessing, and distinguish “confirmed absent” from “not found because coverage was limited”. Tag each field’s evidence as direct (the source states the value), inferred (derived from context) or uncertain (a single indirect signal). That vocabulary is worth adopting in your own prompts because it makes a model’s confidence legible.

Lead lists and enrichment with agent_run

When the job is “find a set of entities and fill in fields about them,” a single search is the wrong tool and agent_run is the right one. The exa-agent skill contains a decision tree that is worth reproducing in spirit:

  • Known input rows plus repeated same-shape enrichment at scale (for example, 400 company names that each need the same five fields): do not burn a model’s context looping over hundreds of identical tool calls. Write a deterministic script that calls the Exa APIs directly, with bounded concurrency, exponential backoff, checkpoints and a stable output file, then read the file and synthesize from it. The skill calls this “Batch Script Mode” and requires that the script “read deterministic inputs from a file,” “use exponential backoff for 429/5xx,” “checkpoint partial progress,” “write deterministic JSON/CSV/TSV output” and “preserve raw API errors per row.”
  • Open-ended universe definition, list-building, discovery, multi-hop research or follow-up over previous work: use agent_run, and define the objective and outputSchema before creating the run.

An illustrative agent_run call for a discovery task:

{
  "query": "Find up to 30 companies building developer tools for API testing, based in North America, that have publicly announced a funding round in the last 18 months. Include the evidence URL for each claim.",
  "effort": "medium",
  "outputSchema": {
    "type": "object",
    "properties": {
      "companies": {
        "type": "array",
        "maxItems": 30,
        "items": {
          "type": "object",
          "properties": {
            "company_name": { "type": "string" },
            "website": { "type": "string", "format": "uri" },
            "hq_region": { "type": "string" },
            "funding_round": { "type": "string" },
            "announced_on": { "type": "string" },
            "evidence_url": { "type": "string", "format": "uri" },
            "confidence": { "type": "string", "enum": ["low", "medium", "high"] }
          },
          "required": ["company_name", "website", "evidence_url"]
        }
      },
      "coverage_notes": { "type": "string" },
      "known_gaps": { "type": "array", "items": { "type": "string" } }
    },
    "required": ["companies", "coverage_notes"]
  }
}

Before creating such a run, the skill says to write down the objective, the universe (what qualifies), the segments (geographies, industries, personas, dates), the coverage target and what “good enough” means, the output fields, the evidence requirements and the exclusions. Note how the example above bakes several of those into the query and the schema: a bounded count, a region, a time window, an evidence field and a confidence field.

After the run, follow the skill’s validation loop:

  1. Wait for the output. If the response has status: "running", call agent_run again with the same runId until outputReady is true. Do not start a duplicate run.
  2. Read both the structured output and the grounding. The skill’s instruction is to read output.structured (the result shaped by your schema) and output.grounding (the citations behind it), not just the first one.
  3. Validate coverage. Check the row count against the target, check segment coverage, deduplicate entities, inspect the evidence quality and identify gaps.
  4. Continue if needed. Use previousRunId to narrow a list, fill missing fields, add another segment, validate a prior set or ask for “more like these”, and use input.exclusion to avoid resurfacing entities you already have. Do not use previousRunId when the prior run failed or is still running, when the new task is unrelated, or when you need clean, independent coverage for another segment; in that case, start separate runs and merge the results yourself.
  5. Report honestly. The skill’s language rules are strict: say “best-effort discovery,” “high-confidence initial universe,” “not exhaustive,” or “coverage appears strongest in X and weaker in Y.” Avoid “all companies,” “complete list,” “exhaustive” or “definitive” unless you have actually validated a bounded universe, the discovery strategy, the counts, the duplicates, the evidence and the remaining unknowns.

That last rule deserves emphasis. A model that ends a lead-list task with “here are all the companies” is making a claim no search tool can support. The skill’s insistence on coverage language is an antidote to the most common failure of research agents: unwarranted completeness.

Using Exa Connect data. When a task needs partner data alongside web search, pass dataSources. For a traffic-based ranking, for instance, the skill’s own example asks for “10 fast-growing B2B SaaS companies” with estimated monthly visits and sets dataSources to [{ "provider": "similarweb" }], with a schema that requires a monthlyVisits number. The lesson generalizes: name the provider-specific field in both the query and the schema so the run uses the provider instead of falling back to generic web search.

Multi-pass research: orchestration for hard questions

Some questions cannot be answered by any single tool call, however well written. The main search skill is an orchestration guide for exactly those, and its structure is a useful template even if you never install the skill.

Step 1: classify the query. The skill defines four levels. Extremely simple (reading one or two pages) is handled directly. Moderate (a fast, low-effort search) can be delegated to one sub-agent to keep the main context clean. Advanced (clear topic, clear filters, a few parallel searches) uses one round of parallel sub-agents. Complex (cross-referencing entity types, multi-hop chains, exhaustive coverage, semantic filtering) uses multiple passes. When a query could plausibly be simple or complex, the skill says to pause and ask the user which they want, explaining what each level would look like in practice.

Step 2: identify the work. The skill lists nine kinds of work and notes that most queries use three to five: seed from user input, define what qualifies, define what to capture, search broadly, extract structured data, filter, merge and deduplicate, score and rank, and synthesize a narrative.

Step 3: dispatch sub-agents with clear instructions. Each gets one sub-question, a pointer to the relevant reference files, a validation criterion so it filters before returning, and an output format. The skill sizes them at three to five searches each and says to parallelize independent workstreams, keeping raw search output out of the main context: “Never run bulk searches in your main context.”

Step 4: compile. Remove exact URL duplicates, merge fields for the same entity from different sources while keeping the most complete and recent data, and record how many results were removed. Then validate coverage: look for obvious gaps in time periods, regions or entity types, and run targeted follow-up searches. Heavy overlap between sub-agents is a good sign; completely disjoint results may mean a missed angle.

Step 5: multi-pass patterns. The skill names three. Entity chaining finds entities in pass one (companies), related entities per result in pass two (people at those companies), and enriches them in pass three (their public statements). Exploratory then targeted scouts the landscape broadly first, then searches deeply where pass one looked promising. Criteria discovery is for “best” questions where the criteria are not predefined: pass one surveys what practitioners actually value, pass two finds candidates matching those criteria.

Step 6: evaluate sources and synthesize. Weight convergence among independent, high-signal sources, because “3 low-quality sources agreeing is just shared noise”. Define who to exclude before synthesizing, since “filtering out noise is more valuable than seeking brilliance”. Red-team the compiled result by asking what perspectives are missing and what biases might distort the aggregate. For narrative answers, lead with the answer, organize by theme rather than by source, surface disagreement explicitly instead of collapsing to a false consensus, include confidence signals (how many independent sources, how fresh, practitioners or commentators), and cite every factual claim with a URL.

The skill also lists gotchas that read like a checklist of ways agents waste money:

Gotcha What goes wrong What to do instead
Over-execution on simple queries Spinning up sub-agents to answer “what year was X founded” One search, one answer
Under-execution on hard queries A single search on a query with four or more constraints or temporal joins Fan out into sub-questions
Synonym queries Running “overrated” and “overhyped” as separate searches Diversify by angle
Forgetting to deduplicate Overlapping results from multiple sub-agents inflate counts Deduplicate before synthesis
Treating results as validated Assuming a result meets the criteria because it was returned Validate against the criteria
Date drift Reusing dates from examples or memory Recompute from the current date

Extraction, filtering and structured output without agent_run

You do not always need the agent. For moderate structured tasks, the extraction and filtering references describe a disciplined approach you can run with the basic tools.

Extract from snippets first. For fields like company name, person name, funding round or publication date, the highlight is often sufficient. Fetch a full page only when the snippet mentions what you need without the value, when you need to read body text to make a judgment, when you need several fields from one rich source such as a team page or filing, or when the task requires reading past the opening sentences.

Apply hard filters before soft ones. Hard filters are binary: a date range, a geography, a numeric threshold, a category membership, an exclusion. Apply them mechanically and cheaply. Soft filters need judgment (“genuine design opinion” versus “generic blog post”) and should be applied only to the survivors, with a one-line rationale per keep-or-drop decision so the reasoning is visible. Semantic negation (“no review mentions noise or smell”) is a soft filter, and requires reading content.

Match the completeness posture to the request. “Find every…” means err toward including borderline cases and flag them as uncertain. “Find the best…” means err toward precision. The default is to include borderline cases with a flag and let downstream processing decide.

Handle missing data honestly. Mark fields “not found” rather than guessing, distinguish “confirmed absent” from “coverage was limited”, and say explicitly when a source is paywalled or inaccessible.

Keeping context and cost under control

Search tools can quietly consume a model’s context. A handful of habits, all consistent with the source, keep that under control.

  • Right-size numResults. Five for a named entity, ten for a filtered query, fifteen for broad discovery, never above 25. If you need more coverage, run more queries from different angles rather than one enormous query.
  • Prefer highlights for discovery and text for reading. The basic search returns highlights. Fetch full text only for the pages you chose.
  • Set maxCharacters on purpose. The 3,000-character default keeps fetches small; raise it for long documents and lower it for quick checks.
  • Batch fetches to reduce round trips, but size the batch to the context you can afford.
  • Offload bulk work to scripts. When the same call repeats hundreds of times, move it out of the model loop, as the skill’s Batch Script Mode prescribes.
  • Choose the smallest tool set that does the job. Every tool description costs context on every turn.
  • Watch costDollars. When the API returns cost information, the advanced tool passes it through in its JSON, which lets a pipeline track spend per call.

A short catalogue of prompts that work well

These are phrased for a general assistant with Exa tools enabled. Adapt the specifics.

Goal Prompt pattern
Verify a claim “Find primary sources for this claim, read the two best, and tell me whether they support it. Quote the relevant line and give the URLs.”
Compare options “Search for practitioner reports of using A and B in production, including complaints and reasons teams switched. Summarize by theme and cite sources.”
Track a topic “Using today’s date, calculate the last 30 days. Find news and practitioner posts about this topic in that window and summarize what changed.”
Build a list “Use Exa Agent to find up to N entities matching these criteria, with an evidence URL and confidence for each. Report coverage and known gaps, and do not claim the list is exhaustive.”
Learn a library “Find the official documentation page for this feature, read it, and show me a minimal example, noting the version the docs describe.”
Diligence a person “Find this person’s public professional work, prefer primary sources, and note where sources are commentary rather than first-hand.”

Each of these encodes at least one of the rules from the bundled guidance: primary sources, explicit dates, independent sources, evidence and coverage language. Prompts that encode those rules tend to produce answers that a human can audit, which is the real test of a research workflow.

Troubleshooting

This section is organized by what you see. Each entry ties the symptom to the behavior in the source, so that you can reason about your own variations rather than only match the ones listed. When in doubt, the first two diagnostics are always the same: read the exa://tools/list resource to see what the session actually registered, and turn on debug logging (DEBUG=true for stdio, debug=true on the hosted URL) to see the enabled tools and the resolved authentication method.

Quick reference

Symptom Most likely cause What to do
Only two tools appear Those are the defaults Enable more with ?tools= or ENABLED_TOOLS, listing every tool you want
I enabled one extra tool and lost the others The tool list replaces the defaults Include web_search_exa and web_fetch_exa in the list too
agent_run is missing (stdio) No user-provided key, so it is not registered Set EXA_API_KEY
HTTP 401 on the hosted URL Auth required, or token invalid or expired Sign in again, refresh, or supply a key; remove the key-requiring tool if you want anonymous access
“You’ve hit Exa’s free MCP rate limit” Anonymous per-IP limit reached Add your own key, preferably by header
“timed out after 60s” Search or fetch exceeded its timeout Narrow the query, fetch fewer URLs, retry
Advanced search is slow Subpage crawling or live fetches Reduce subpages, drop maxAgeHours: 0, or accept the 300s ceiling
Fetch returns a short excerpt Default maxCharacters is 3,000 Pass a larger maxCharacters
“No search results found” Query too short, too narrow, or over-filtered Lengthen, change angle, loosen filters
A category: filter seems ignored Category not supported inline, or misspelled Use the five inline categories, or the advanced tool
Stale results Cached content or undated query Use maxAgeHours: 0 and explicit dates
Old tutorial’s tool names do not exist Tools were deprecated Use the migration map
Server will not start in a GUI client PATH or Node version Absolute path to npx; Node 20 or newer

“I only see two tools” and “I lost my tools”

This is the most common confusion and it is entirely by design. The default set is web_search_exa and web_fetch_exa. When you specify a tool list, either as the tools query parameter or the ENABLED_TOOLS environment variable, the server registers exactly those and nothing else. So ?tools=web_search_advanced_exa gives you the advanced search tool alone, without the basic search or fetch tools. The README says as much: “this will replace the defaults, so include all you want.”

Two related traps. First, unknown names are dropped silently. The expansion function skips any value that is not in the registry, so a typo such as web_seach_exa does not raise an error; the tool simply is not there. If a tool you named is absent, compare your spelling to the IDs in the registry table earlier in this guide. Second, if your list contains only unknown names, parsing yields an empty list and the defaults apply again, on the stdio server and on the hosted handler alike, which can look like your configuration was ignored.

“agent_run does not show up”

agent_run is opt-in and requires authentication. On the local stdio server, it is registered only when EXA_API_KEY is set (which marks the key as user-provided). Without a key, naming it in ENABLED_TOOLS has no effect. On the hosted endpoint, requesting it without credentials gets an HTTP 401 with an OAuth challenge, which an OAuth-capable client should turn into a sign-in prompt. If your client does not, or you are scripting, supply a key with the x-api-key header.

HTTP 401 on the hosted endpoint

A 401 from mcp.exa.ai is not always a failure. Sometimes it is the start of the OAuth flow. The handler returns 401 with a WWW-Authenticate: Bearer header pointing at OAuth protected-resource metadata in these situations:

  • The URL includes ?login, or you connected through /mcp/oauth, both of which force sign-in.
  • The URL carries a client parameter containing plugin (as the official plugins do), which forces sign-in.
  • The client’s user agent matches a configured OAuth-forcing list (an operator setting).
  • The request names a tool that requires a user-provided key, such as agent_run, and presents no credentials.
  • A bearer token that looks like a JWT failed verification, meaning it is expired, has a bad signature or has the wrong issuer or audience. In this case the challenge includes error="invalid_token", which is the standard signal for the client to refresh or re-authenticate.

What to do depends on the cause. If you expected anonymous access, remove ?login and any key-requiring tools from the URL. If you expected to be signed in, re-run the client’s authentication (in Claude Code, the /mcp command) and make sure the client is not caching an old token. If you are scripting with an API key, send it as x-api-key; sending a JWT-shaped string in a bearer header would be interpreted as an OAuth token and rejected if it is not one.

“You’ve hit Exa’s free MCP rate limit”

This is the message the model receives when an anonymous session gets an HTTP 429. It reads, in part, “To continue using without limits, create your own Exa API key,” and tells the model to change the MCP URL to include your key. The limiter is per IP, and it applies only to tool calls, not to protocol methods like listing tools. Two practical points follow. If several people share an office or VPN egress address, they share an anonymous allowance, so a limit can appear “for no reason” on a shared network. And since the message was written for a model, a good agent will relay the fix to you; if yours does not, the fix is the same: create a key in the dashboard and supply it, preferably in the x-api-key header instead of the URL.

Timeouts and transient errors

There are three timeouts in the source: 60 seconds for web_search_exa, 60 seconds for web_fetch_exa and 300 seconds for web_search_advanced_exa, each producing an error such as “web_search_exa timed out after 60s”. Separately, upstream HTTP 500, 502, 503 and 504 responses are retried up to two times with exponential backoff before an error is returned, so a single blip is usually invisible to you. When you do see a timeout:

  • For fetches, reduce the number of URLs per call, or fetch the slowest one separately.
  • For advanced search, reduce subpages, remove maxAgeHours: 0 (which forces a live fetch), or lower numResults.
  • For anything, retry once. If a particular URL times out repeatedly, it may be slow or blocking automated access; the search skill’s advice is to fall back to another fetch tool and, failing that, skip the source.
  • For agent_run, remember that a run outliving the call window returns a running status and a run ID. That is not a timeout; call again with runId.

If you run behind a reverse proxy, check its read timeout. A proxy that cuts connections at 60 seconds will make long calls fail in ways that look like server errors.

Empty results

“No search results found. Please try a different query.” is a valid answer, not an error. The bundled guidance for handling it is specific: make the query longer and more specific; try a different angle, not a synonym; and if several angles return nothing, report that the topic likely has limited web coverage instead of fabricating results. Also check that filters are not the cause. On the advanced tool, includeText requires results to contain all of the strings you give, so a list of five strings is a very narrow filter, and a date window combined with a narrow category can leave nothing.

Fetches that seem truncated

If a fetched page ends abruptly, you are almost certainly hitting the 3,000-character default. Pass maxCharacters explicitly. If the page is long and you need all of it, choose a value that comfortably exceeds its length, and if you are worried about context, fetch in a separate call from other pages. If you need the page in chunks, note that the tool does not paginate; a general-purpose fetch server with pagination (see the Comparison section) is the complement for that.

If a fetch returns an error line for a URL, the tag after the URL comes from Exa’s per-URL status. Typical causes for any content extraction service are pages behind logins or paywalls, pages that block automated access, and pages that fail to load; the tool cannot do anything about those, and the search skill’s fallback advice applies.

Categories that do not seem to work

The basic tool recognizes five inline categories: company, publication, news, personal site and people. Anything else, such as category:github, category:pdf or category:financial report, is not extracted and remains part of the query text. Those three exist only on web_search_advanced_exa, through its category parameter. Also, the inline token must be spelled exactly; category:companies will not match the pattern.

Results look stale

Two causes. Exa may be serving cached content, which the advanced tool lets you control with maxAgeHours: 0 always fetches fresh content, and omitting it uses cache with a live fetch as a fallback. And the query itself may not express recency; for the basic tool, encode dates in words (“published in March 2026”) after computing them from the current date, and for hard windows use the advanced tool’s date filters.

Tool names from older tutorials

If a tutorial or an old config uses get_code_context_exa, company_research_exa, crawling_exa, people_search_exa, linkedin_search_exa, deep_search_exa or the deep-researcher tools, be aware they are deprecated. They still register if you name them explicitly, which keeps old configs functional, but they are not in the default set and will not appear if you use the defaults. Use the migration map in the Features section.

The local server will not start

  • Node version. The package requires Node 20 or newer. Check node --version in the same environment the client uses.
  • npx not found. GUI clients often lack your shell’s PATH. Use the absolute path from which npx.
  • Environment not passed. Make sure EXA_API_KEY is in the client’s env block, not only in your terminal.
  • Stale cache. With npx -y, a cached older version can be used. Pin a version (exa-mcp-server@3.4.1) or clear the npx cache when debugging.
  • Wrong entry point. The package’s binary is dist/stdio.cjs, generated at build time. If you run from a clone rather than from npm, run npm ci and npm run build first, or use npm run dev (tsx src/stdio-cli.ts) for development.

The client complains about the transport

The repository is slightly inconsistent about how it names the hosted transport. server.json lists the remote with type sse, while the README and the plugin manifests configure streamable-http (or http in Claude Code’s dialect). If your client insists on a particular type, use the value the client documents for Streamable HTTP servers; the URL is the same. Prefer Streamable HTTP wherever the client offers it.

Self-hosted instance problems

  • /ping works but the MCP endpoint does not. Use /mcp (or /); the handler maps a specific set of paths onto its internal route. Also check that your proxy forwards the Mcp-Session-Id and MCP-Protocol-Version headers.
  • Browser clients fail with CORS errors. The handler advertises a specific allowed header list. A proxy that rewrites or drops CORS headers will break browser-based clients.
  • Anonymous callers are burning my key. That is the runtime server’s default-key behavior: with EXA_API_KEY set, callers presenting no credentials use it. Remove the default key or authenticate callers upstream.
  • No rate limiting. Limits exist only when Redis-compatible credentials are configured.

Comparison: Exa and the other ways to give an agent the web

Choosing a web-access tool is a design decision with real trade-offs, not a ranking. This section describes categories of alternatives and where each fits. It does not claim that any option is faster, cheaper or more accurate, because this guide contains no measurements, and you should not trust anyone’s claims about search quality that are not reproducible on your own queries.

What Exa MCP is optimized for

From its tool descriptions and bundled guidance, Exa MCP is built around four ideas. Search is semantic: you describe the ideal page and the index returns nearest neighbors, so natural-language descriptions work better than keywords. Results arrive agent-ready: highlights or clean text, not raw HTML. Filters are first-class: categories, domains, dates, text inclusion and exclusion, freshness and subpage crawling on the advanced tool. And there is a research-agent option for multi-step, structured, list-building work. It does not click buttons, log into accounts, execute JavaScript-driven flows or interact with pages. It reads the public web and returns content.

Against other search servers

Other registry entries offer search through different providers. The Brave Search server, for example, is listed as offering “web, local, news, image and video search through the Brave Search API” with five tools (brave_web_search, brave_local_search, brave_news_search, brave_image_search and brave_video_search), runs over stdio, is MIT-licensed and needs an API key. That breadth of result types is a genuine differentiator: if your agent needs image results, video results or local business results, a server that exposes those types directly is the natural fit, whereas Exa’s current default tools center on web pages, page contents and research runs. Conversely, if your queries are descriptive and your goal is agent-ready page content with filtering and structured research, Exa’s tool shape is the closer match. Many teams reasonably enable both and let the model pick, at the cost of a longer tool list.

Against a general-purpose fetch server

The reference fetch server in the registry is described as a “reference server that fetches a URL and converts the page to markdown for a model to read, with optional raw mode and pagination through long pages.” That is a different tool: it takes a URL you already have, runs on your machine, and lets you page through long documents. Exa’s web_fetch_exa also reads URLs, but the extraction happens on Exa’s side, it caps characters per page, and it does not paginate. The two are complements more than competitors. Use Exa to find pages and a fetch server when you need pagination, raw content, or a request that originates from your own network (for example, a page reachable only from your environment, which a third-party service cannot fetch).

Against browser automation

Playwright’s MCP server exposes a real browser: navigate, snapshot, click, type, take screenshots, list tabs, read console messages and network requests. That is the right tool for pages that require interaction, authentication, client-side rendering or multi-step flows, and for testing your own web applications. It is also heavier: each step is a tool call, and pages can produce large snapshots. For reading public content, search plus fetch is usually the cheaper path. The natural division of labor is Exa for discovery and reading, browser automation for the pages that need a human-like session.

Against documentation-specific servers

Context7 is listed in the registry as “up-to-date code docs for any prompt,” with two tools (resolve-library-id and get-library-docs) and a hosted Streamable HTTP endpoint. A documentation server focused on library docs is likely to be more precise for “how do I use this library version” questions than a general web search. Exa is broader: it can also find the GitHub issue, the migration blog post or the conference talk that the documentation does not contain. Pairing them is reasonable: documentation server first for API reference, Exa for everything around it.

Against built-in search in an assistant

Some assistants have native web search. If yours does and it meets your needs, you may not need an MCP server at all. An MCP-based search tool earns its place when you want portability across clients and models, explicit control over filters and categories, a structured research tool, consistent behavior in automated pipelines, or the ability to audit exactly which tool was called with which arguments. It costs you a tool list entry, a credential and a dependency.

Against writing your own scraper and search stack

Building your own crawler, index and extraction is possible and occasionally warranted, for instance when data must never leave your network. It is also a long-term commitment: coverage, freshness, deduplication, content extraction quality, anti-bot handling, legal review and operations all become yours. For most agent projects, a hosted API behind an MCP server is the pragmatic choice, with self-hosting of the MCP layer (which this project supports) as the compromise when you need control over the interface but not the index.

A decision table

If you need… Consider
Descriptive, semantic discovery of pages with agent-ready content Exa web_search_exa
Hard filters on domain, date, category or text Exa web_search_advanced_exa
Structured, multi-step research or list-building Exa agent_run
Image, video or local results A server that exposes those result types
Pagination through long pages, or fetching from your own network A general-purpose fetch server
Clicking, logging in, or interacting with a page Browser automation
Official library API documentation A documentation-focused server, with Exa as a complement
Everything self-contained and offline Neither; build or buy an internal index

How to evaluate for yourself

Because no article can settle quality questions for your workload, run a small, honest evaluation. Collect 30 to 50 real queries from your users or your team, spread across the categories you care about (documentation lookups, company questions, news, people, niche technical topics). For each candidate setup, record the tool calls made, the pages surfaced, the latency and any cost. Have two people judge the final answers blind, scoring correctness, citation quality and whether the answer admitted uncertainty where appropriate. Include a few “there is no good answer” queries to see which setup fabricates. Repeat it when tools or versions change. That exercise will tell you more than any comparison, including this one.

Security, privacy and governance

Search-enabled agents combine three risky ingredients: untrusted content, powerful tools and sensitive context. Exa MCP is a read-oriented server, so its direct blast radius is smaller than a server that can write files or run commands. But it is precisely the component that brings arbitrary internet text into your model’s context, and that makes it a security-relevant piece of your agent.

Treat everything returned as untrusted input

A web page can contain text written to manipulate an AI assistant: instructions to ignore prior guidance, to reveal secrets, to call other tools, or to fetch a URL that exfiltrates data. This class of attack is called prompt injection, and no search or fetch tool can fully prevent it, because the malicious text is legitimate page content. Your defenses are architectural:

  • Least privilege. An agent that has search tools and the ability to send email, run shell commands or write to production systems is a bigger risk than one that can only read. Where you combine them, require human confirmation for side-effecting actions.
  • Separate roles. Consider a research agent that has Exa tools but no write tools, whose output (a summary with citations) is handed to a second agent or a human for action.
  • Confirmation for surprising actions. If a page’s content leads the model to propose an unrelated action, treat that as a red flag, not a plan.
  • Scope the sources. On the advanced tool, includeDomains restricts results to domains you trust, which is a strong control when your questions are about known sources.
  • Do not confuse moderation with injection defense. The advanced tool’s moderation option filters out unsafe or inappropriate content. That is a content-safety feature, not a defense against manipulative instructions.
  • Review code and commands from the web before running them. Search results for programming questions will include copy-pasteable commands. They are suggestions.

The hints the server sets (readOnlyHint, destructiveHint: false, idempotentHint, openWorldHint) help clients present sensible approval prompts, but they say nothing about the safety of the content returned.

Credentials

  • Prefer OAuth. It keeps long-lived secrets out of config files and is the README’s recommendation.
  • Prefer headers to URLs. A key in ?exaApiKey= can end up in shell history, screenshots, proxy logs, browser history and shared documents. The hosted handler strips the query parameter and the credential headers before passing the request to its analytics-wrapped MCP layer (its own comments explain this: the analytics wrapper “captures HTTP headers, query params, and the full URL from every request”, so credentials are removed first), but that protects one hop, not your own logs.
  • Use environment expansion. In clients that support it (Claude Code’s .mcp.json does), reference ${EXA_API_KEY} so the file can be committed.
  • Rotate and scope. Treat the key like any API credential: create separate keys per environment, rotate on suspicion, revoke unused ones.
  • Never paste keys into prompts. A model does not need your key; the client injects it.

What leaves your machine

Understanding the data flow lets you decide what is acceptable for your context.

  • Queries and URLs go to Exa. With the hosted endpoint, your MCP client talks to Exa’s server, which calls the Exa API. With the local stdio server, your machine calls the Exa API directly. With a self-hosted runtime, your infrastructure calls the Exa API. In every case, the query text and any URLs you ask it to fetch reach Exa. Do not put secrets, personal data or confidential identifiers in queries, and do not ask the tool to fetch URLs that contain tokens (signed links, password-reset links, session parameters).
  • Attribution headers accompany requests. The server labels traffic with x-exa-integration (a tool-specific label such as web-search-mcp), can include an x-exa-source value, an MCP session ID and serialized MCP client metadata (client name and version, gathered from the protocol’s initialize step). On the hosted service, session-scoped client metadata is cached in Redis, when configured, so later requests in the same session can carry it.
  • Analytics. The server initializes analytics tracking from the agnost package with logs, input, output and error capture disabled in its configuration, and the hosted handler removes credentials before the analytics layer sees the request. If your policy forbids any third-party analytics dependency, self-hosting with that dependency removed, or a review of the current source before adoption, are the ways to be certain.
  • Retention. The bundled agent skill mentions Zero Data Retention teams and how runs behave for them, which indicates that retention is an account-level matter with Exa. If retention matters to you, ask Exa directly and read their current terms; this guide cannot answer it.

Rate-limit and cost governance

Anonymous access is rate limited by IP, and keyed access is governed by your Exa account. For teams, decide who holds keys, whether developers share one or use individual keys, and how you will notice runaway usage. The advanced tool passes through costDollars when the API provides it, which you can log. An agent_run with a high effort and a broad objective is an obvious place for cost to accumulate, so consider capping effort in shared configurations by controlling which tool names you expose and documenting expectations for their use.

Supply chain and change management

  • Pin versions. npx -y exa-mcp-server will run whatever version is current. For reproducible, reviewable setups, pin (exa-mcp-server@3.4.1) and update deliberately after reading changes.
  • Know the dependency set. The package’s runtime dependencies include the MCP SDK, exa-js, zod, jose, mcp-handler and agnost. Your security scanner will see these.
  • Prefer the hosted URL only if you accept remote changes. A hosted endpoint changes without your involvement; a pinned local package changes when you decide.
  • Watch for tool-list drift. The deprecations in this project show how tool sets evolve. If your agent’s behavior depends on a specific tool, test after upgrades.

Self-hosting hardening checklist

  1. Run the container as non-root (the provided image does).
  2. Terminate TLS at a proxy and require authentication in front of the runtime, or leave EXA_API_KEY unset so callers must bring their own.
  3. Restrict egress to the Exa API host and your authorization server.
  4. Configure Redis-backed rate limiting or enforce limits at the gateway.
  5. Set proxy timeouts above the longest expected call and disable response buffering for the MCP path.
  6. Log request metadata (caller, tool, latency) but not query text, unless policy requires it.
  7. Rebuild the image on a schedule to pick up base-image and dependency updates.

Legal and content considerations

Pages returned by a search or fetch tool remain subject to their owners’ terms and copyrights. Using content to answer a question inside your organization is one thing; republishing it wholesale is another. Respect site terms, robots directives and licensing, credit sources when you publish summaries, and involve counsel if you are building a product that redistributes fetched content. That advice is generic to any web-access tool, and it applies here because the point of this server is to bring web content into your workflows.

Advanced topics

The earlier sections cover what most users need. This one is for people who want to understand the design in more depth: what the response sanitizer keeps, what the project teaches about building good MCP servers, how long-running work is handled, how to tune and observe a deployment, how to upgrade safely, and how to extend or fork the code.

What the response sanitizer keeps

Every tool passes the upstream API response through functions in src/utils/exaResponseSanitizer.ts before formatting it. The design is a whitelist: rather than deleting a list of known-bad fields, the sanitizer builds a new object from the fields it explicitly recognizes and ignores everything else. The one explicit deny-list is a single key, requestTags, which is stripped wherever it appears in nested objects.

At the top level of a search response, the sanitizer keeps these fields when they have the expected type:

Field Type kept Meaning (as far as the source shows)
requestId string An identifier for the API request
autopromptString string A rewritten query string, if the API produced one
autoDate string A date the API inferred, if any
resolvedSearchType string The search type the API actually used
context string A combined context string, when requested
output object Structured output, reduced to content and grounding
statuses array Per-item statuses, reduced to id, status and source
results array The results, each sanitized as below
searchTime number Time the search took
costDollars object Cost information, with sensitive keys stripped

Each result keeps: id, url, publishedDate, author, text, summary, image and favicon (all strings), title (a string or null), score (a number), highlights (string array), highlightScores (number array), entities (an array of objects, with sensitive keys stripped), extras (reduced to links and imageLinks string arrays) and subpages (each sanitized recursively). Arrays are filtered so that only elements of the expected type survive, and empty arrays are omitted entirely.

Inside output, content is passed through with sensitive keys stripped, and grounding entries are rebuilt to contain only a list of citations (each reduced to a URL and title, both required strings) plus an optional field string and an optional confidence string.

What does this tell you? Three practical things.

  • New API fields do not automatically reach the model. If Exa adds a field to its API, it will not appear in tool output until the whitelist is updated. That keeps responses predictable and small, at the cost of lagging behind the API.
  • Internal or noisy fields are kept out of the context window. Removing requestTags and unknown keys means less text for the model to read and fewer accidental leaks of internal metadata.
  • It is a sound pattern to copy. For any MCP server that proxies a third-party API, an allow-list sanitizer is safer than returning the raw response. The API can change, and a raw passthrough turns every upstream change into a change in your tool’s contract.

There is also a variant for deep search (sanitizeDeepSearchStructuredResponse) that preserves the original output.content without the stripping, so that structured output is not mangled, and a wrapper for the contents endpoint (sanitizeContentsResponse) that reuses the same top-level logic.

Design lessons for MCP server authors

Whether or not you ever use Exa’s server, its codebase is a compact, well-organized example of production MCP practice. Here are the patterns worth borrowing, each tied to where it appears.

1. Keep the default tool set small. The registry enables two tools by default. The costs of a longer list (context tokens on every turn, more decisions for the model, more surface to secure) are real, and opt-in URLs and environment variables let power users expand it.

2. Write tool descriptions as prompts. Look at the pattern in the descriptions: a one-line purpose, then “Best for:”, then “Returns:”, then “Query tips:”, and, for the fetch tool, “Use after web_search_exa when highlights are insufficient.” The descriptions tell the model when to use the tool, what it gets back and how to chain it with the next one. That is prompt engineering embedded in the tool contract.

3. Be lenient in what you accept. The lenient* validators coerce numbers to strings, strings to numbers and common boolean spellings, and swallow invalid optional values instead of failing. Models make small type slips. A tool that forgives them wastes fewer turns.

4. Return failures as tool results the model can act on. formatToolError produces text like “You’ve hit Exa’s free MCP rate limit… Fix: Create API key at …” with isError: true. The message names the problem, the remedy and the exact URL change. Compare that with a bare “429.”

5. Retry only what is retryable, and bound everything. Retries apply only to 500, 502, 503 and 504, capped at two with exponential backoff; every call has a timeout. Errors like 401 and 429 are not retried, because retrying will not change the outcome.

6. Annotate honestly. Read-only, non-destructive, idempotent and open-world hints tell clients how to treat the tool. (The one inconsistency, openWorldHint: false on the advanced tool, is a reminder to review annotations when you add tools.)

7. Deprecate by aliasing, not by deleting. crawling_exa is registered as an alias of web_fetch_exa, the legacy agent tool names map to agent_run, and old tools remain available behind explicit opt-in. Existing configs keep working while new users get a clean default.

8. Isolate per-request configuration. The hosted handler builds a fresh MCP handler per request so one caller’s key and tool list cannot bleed into another’s. If your server is multi-tenant, that is the property to preserve.

9. Sanitize by whitelist. Covered above.

10. Make long-running work resumable. Covered next.

11. Ship guidance alongside the tools. The skills directory turns institutional knowledge (how to write queries, how to design schemas, how to validate coverage) into files that clients can load. Servers usually ship only tools; shipping guidance is a differentiator.

12. Separate transport from core. One initializeMcpServer function serves stdio, the hosted handler and the self-hosted runtime. The transport code is thin.

Long-running work: the run-and-resume pattern

MCP tool calls are synchronous from the model’s perspective, but useful research can take minutes. The agent_run design is a clean answer to that mismatch, and it generalizes to any long task.

  1. Start and stream inside a bounded window. The call starts a run and streams events for up to the call window, which is derived from the platform’s maximum function duration minus a headroom (800 seconds minus 50 seconds, so 750, by default). A heartbeat every 15 seconds keeps the connection alive and gives progress to clients that display it.
  2. Return a handle, not a failure, when the window closes. If the run has not finished, the tool returns a running status with a runId. Nothing is lost.
  3. Resume by handle. Calling the tool again with runId checks the run and continues waiting. The tool’s parameter description warns against starting a duplicate.
  4. Fail safe on abort. If the client aborts the call, the server attempts to cancel the upstream run so that abandoned work does not keep consuming resources.
  5. Protect the session from slow clients. Progress notifications get a two-second delivery budget; if the client does not accept them in time, the emitter disables itself rather than blocking the run.
  6. Compose runs. previousRunId lets a follow-up run start from a completed one, and input.exclusion lets you say “not these again.”
  7. Document the exception. For Zero Data Retention accounts, output is only available on the live stream, so resumption is impossible. The skill says so plainly and gives the alternative (lower effort, or split the task).

If you are building a server with long operations (report generation, batch jobs, crawls), the same recipe applies: bound the synchronous window, return an identifier, make status checks idempotent, cancel on abort, and document the special cases.

Tuning and observability

What you can tune:

Knob Where Effect
DEFAULT_SEARCH_TYPE / defaultSearchType env, hosted URL auto for quality, fast and instant for speed
numResults per call Result count, and therefore context size
Tool set tools, ENABLED_TOOLS Context cost and behavior surface
maxCharacters, textMaxCharacters per call Length of extracted text
maxAgeHours, livecrawlTimeout advanced tool Freshness versus latency
subpages, subpageTarget advanced tool Crawl depth versus latency
effort agent_run Depth of a research run
MCP_MAX_DURATION_SECONDS, AGENT_CALL_WINDOW_MS env How long an agent call waits

What you can observe. The search tools attach the measured search time to the response as _meta.searchTime. The advanced tool’s JSON includes searchTime and may include costDollars. Debug logging prints the enabled tool list and the authentication method. If you call the server from your own client, wrap each call to record tool name, arguments (subject to your privacy policy), latency, isError and response size. A simple dashboard of those five columns over a week answers most tuning questions: which tools are used, where latency comes from, how often rate limits or timeouts hit, and how large responses are.

Tuning strategy. Start with defaults and change one thing at a time. If answers are shallow, encourage the search-then-fetch loop before you raise numResults. If latency is the problem, try a faster search type on a self-run server before removing features. If context bloat is the problem, reduce numResults, lower maxCharacters for exploratory fetches, and trim the tool list. If cost is the problem, look at agent_run usage first, then at whether scripts could replace repetitive tool loops.

Upgrading safely

The registry-versus-repository drift described at the start of this guide is a general lesson about MCP servers: catalogues are snapshots, and projects evolve. A safe upgrade routine looks like this.

  1. Find the current version. Check the project’s package.json and server.json on GitHub, or npm view exa-mcp-server version for the published package.
  2. Read what changed. Compare the tool registry between versions. Additions, deprecations and default changes are what affect agents.
  3. Pin and stage. Pin the version in your client configuration, upgrade in a development profile first, and read exa://tools/list to confirm the expected tool set.
  4. Replay a fixed set of prompts. Keep a small suite of representative prompts and compare tool calls and answers before and after.
  5. Migrate deprecated calls. Replace deprecated tool names in prompts, saved workflows and documentation.
  6. Update your own documentation. If your team wiki lists Exa tools by name, refresh it.

For the hosted endpoint, you cannot pin the server, so rely on the tool list you request and on the replay suite to detect drift.

Extending or forking the server

The codebase is small enough to fork comfortably. To add a tool, you would:

  1. Create a file under src/tools/ exporting a register...Tool(server, config) function that calls server.tool(name, description, zodShape, annotations, handler).
  2. Add an entry to TOOL_REGISTRY in src/toolRegistry.ts with a name, description, enabled default, group and, if needed, requiresUserProvidedApiKey.
  3. Add a guarded if (canRegisterTool("your_tool")) block to initializeMcpServer in src/mcp-handler.ts.
  4. Route the API call through createExaClient and integrationHeaders (or your own client), wrap it with retryWithBackoff and withTimeout, sanitize the response and format errors with formatToolError.
  5. Write unit tests. The repository uses Vitest, with a tests/helpers/fakeMcpServer.ts helper and test files mirroring the tool and utility files. The ci script runs the format check, the type check and the tests.

Keep the license in mind: MIT permits forking and modification with attribution. If you fork to change the analytics dependency, add authentication in front of the runtime or restrict tools for your organization, keep the changes small and rebase regularly, since the tool registry moves.

A simpler, often better alternative to forking is to leave the server alone and add your policy at the client or gateway layer: restrict which tools are exposed through tools, put an authenticating proxy in front of a self-hosted runtime, and log calls in your own client.

Using the skills without the plugin

The search and exa-agent skills are plain markdown files with front matter. If your client does not support Agent Plugins, you can still use them as prompt templates: copy the relevant file into your agent’s instruction set or project documentation, and include the reference files (searching.md, filtering.md, extraction.md, synthesis.md, source-quality.md) when the task calls for them. Note that the search skill’s orchestration steps assume a client that can dispatch sub-agents and that it tells sub-agents to use only web_search_exa and web_fetch_exa; adapt those instructions if your tool set differs.

A sketch of a batch enrichment script

The exa-agent skill says to write a script, not loop through tool calls, when you have many rows of the same shape. Here is a structural sketch in TypeScript that shows the required properties: deterministic input, bounded concurrency, backoff on 429 and 5xx, checkpoints and a stable output file. The enrichRow function is intentionally abstract; fill it with a call to the Exa API or to an MCP client.

import { readFileSync, writeFileSync, existsSync } from "node:fs";

type Row = { id: string; company: string };
type Result = { id: string; ok: boolean; data?: unknown; error?: string };

const CONCURRENCY = 5;
const OUT = "enriched.json";

async function withBackoff<T>(fn: () => Promise<T>, tries = 5): Promise<T> {
  let delay = 1000;
  for (let attempt = 1; ; attempt++) {
    try {
      return await fn();
    } catch (err: any) {
      const status = err?.status ?? err?.statusCode;
      const retryable = status === 429 || (status >= 500 && status <= 504);
      if (!retryable || attempt >= tries) throw err;
      await new Promise((r) => setTimeout(r, delay));
      delay *= 2;
    }
  }
}

async function enrichRow(row: Row): Promise<unknown> {
  // Call the Exa API (or an MCP client) here and return structured fields.
  throw new Error("implement me");
}

async function main() {
  const rows: Row[] = JSON.parse(readFileSync("input.json", "utf8"));
  const done: Record<string, Result> = existsSync(OUT)
    ? Object.fromEntries((JSON.parse(readFileSync(OUT, "utf8")) as Result[]).map((r) => [r.id, r]))
    : {};

  const queue = rows.filter((r) => !done[r.id]);
  let inFlight = 0;
  let index = 0;

  await new Promise<void>((resolve) => {
    const pump = () => {
      if (index >= queue.length && inFlight === 0) return resolve();
      while (inFlight < CONCURRENCY && index < queue.length) {
        const row = queue[index++];
        inFlight++;
        withBackoff(() => enrichRow(row))
          .then((data) => (done[row.id] = { id: row.id, ok: true, data }))
          .catch((e) => (done[row.id] = { id: row.id, ok: false, error: String(e?.message ?? e) }))
          .finally(() => {
            inFlight--;
            writeFileSync(OUT, JSON.stringify(Object.values(done), null, 2)); // checkpoint
            pump();
          });
      }
    };
    pump();
  });
}

main();

The script is resumable (it skips rows already present in the output file), records per-row errors instead of aborting, and produces a stable file that a model can read afterwards. That last step is the point: the model’s job is to interpret the output, not to perform four hundred identical calls.

Frequently asked questions

What is the Exa MCP server? It is an MCP server, published by Exa Labs under the MIT license, that gives AI clients tools for web search, reading web pages as markdown, filtered advanced search and multi-step research runs, backed by the Exa search API.

Do I need an API key? Not to try the hosted endpoint. The README says it works anonymously with rate limits. For higher limits, and for agent_run, you need OAuth or an API key. For the local stdio server you will want EXA_API_KEY.

Which URL do I use? https://mcp.exa.ai/mcp.

Why do I only see two tools? Those are the defaults, web_search_exa and web_fetch_exa. Enable more with ?tools= or ENABLED_TOOLS, and list every tool you want because the list replaces the defaults.

What happened to get_code_context_exa and company_research_exa? They are deprecated. Use web_search_exa (and its category: tokens) or web_search_advanced_exa instead.

How many characters does web_fetch_exa return? By default, up to 3,000 characters per page. Set maxCharacters for more.

Is the server read-only? Yes. All the search and fetch tools are declared read-only and non-destructive, and the code only issues search and content requests. It still reaches the public web and returns untrusted content, so treat results accordingly.

Where do my queries go? To Exa. In hosted mode through Exa’s MCP server, and in stdio or self-hosted mode from your machine or infrastructure to the Exa API. Do not put secrets in queries.

Can I run it entirely inside my network? You can self-host the MCP layer using the provided Dockerfile, but it still calls the Exa API. The search index is not something the container includes.

Does it work with Claude Code, Cursor, VS Code, Codex and Gemini CLI? Yes, through the configurations described in the Integration section. Claude also has a plugin, and the README lists many other clients.

How do I enable the research agent? Add agent_run to your tool list (or use the alias agent_tools) and authenticate with OAuth or an API key.

Is it safe to let an agent use it autonomously? The tools cannot change anything, but the content they return can try to manipulate the model. Combine autonomous search with least-privilege tools and confirmation for side effects.

Where can I find similar servers? The MCP Registry lists more than 32,000 servers; browse the catalogue or ask the registry’s own MCP endpoint at https://ai.mcpharbor.dev/mcp.

Conclusion

The Exa MCP server is best understood as a small, deliberate set of read-only tools on top of a semantic search API, wrapped in careful engineering: a two-tool default, forgiving validation, whitelist sanitization, bounded retries and timeouts, actionable errors, per-request isolation on the hosted endpoint, and an unusually thorough set of bundled guides that teach an agent how to use the tools well. Its opt-in advanced search and agent tools extend it from “find and read pages” to “filter precisely” and “run a multi-step, structured investigation,” without burdening the default experience.

If you take five practices away from this guide, make them these. Describe the page you want and let the search step discover candidates, then read the best few with web_fetch_exa and a deliberate maxCharacters. List your tools explicitly, remembering that the list replaces the defaults. Prefer OAuth, or a header-borne key, to a key in a URL. Treat everything the tools return as untrusted input. And when a task is about coverage, use the coverage language: say “best-effort”, state the gaps and never claim a list is exhaustive.

One last reminder about sources. Catalogues, tutorials and even this guide are snapshots of a moving project. The authoritative descriptions are the repository’s README and source, and the tool list your own session reports at exa://tools/list.

Where to go next

This article was prepared for the MCP Registry from the public repository and website of the Exa MCP server. Product names and trademarks belong to their owners. Behavior described here reflects the source at version 3.4.1 and may change in later releases.