> ## Documentation Index
> Fetch the complete documentation index at: https://docs.get-exo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Claude Code

> Connect Claude Code to your Exo knowledge graph over hosted MCP, using a key you mint yourself. No install, no local server.

Claude Code reaches Exo over MCP Streamable HTTP at `https://api.get-exo.com/mcp/`, authenticated with a static Exo API key. After this page, Claude Code will call `exo_context` before answering questions about your own history, and `exo_capture` after substantive exchanges.

## Before you start

* An Exo account. Exo is invite only during the private preview, so sign-in fails with a waitlist message if your account is not allowlisted yet.
* An Exo API key. Mint one on the dashboard, or with `POST /v1/keys` if you already have a key with the `admin` scope. See [Authentication](/authentication).
* Claude Code installed and able to reach the public internet.

## The short version

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
claude mcp add exo --transport http \
  https://api.get-exo.com/mcp/ \
  --header "Authorization: Bearer exo_prod_your_key_here"
```

Restart Claude Code, then run `claude mcp list` and confirm the `exo` entry shows a check mark.

<Warning>
  Keep the trailing slash on `/mcp/`. A bare `/mcp` returns a 307 redirect, and MCP clients drop the `Authorization` header across a redirect, which surfaces as a 401 you cannot explain from the config.
</Warning>

## Set it up

<Steps>
  <Step title="Mint a connection key">
    Sign in to [the Exo dashboard](https://get-exo.com) and open **Import** then **Connect Claude Code**. Click **Generate connection key** and copy the key. The plaintext is shown once and is never retrievable again, because Exo stores only its SHA-256 hash.

    If you would rather do it over the API, mint a narrowed key so a coding agent cannot erase content:

    <CodeGroup>
      ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      curl -X POST https://api.get-exo.com/v1/keys \
        -H "X-Exo-API-Key: $EXO_KEY" \
        -H "Content-Type: application/json" \
        -d '{"label": "Claude Code", "scopes": ["read", "graph:write"]}'
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      import os, requests

      r = requests.post(
          "https://api.get-exo.com/v1/keys",
          headers={"X-Exo-API-Key": os.environ["EXO_KEY"]},
          json={"label": "Claude Code", "scopes": ["read", "graph:write"]},
      )
      print(r.json()["apiKey"])  # shown once
      ```

      ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      const res = await fetch("https://api.get-exo.com/v1/keys", {
        method: "POST",
        headers: {
          "X-Exo-API-Key": process.env.EXO_KEY!,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          label: "Claude Code",
          scopes: ["read", "graph:write"],
        }),
      });
      const { apiKey } = await res.json(); // shown once
      ```
    </CodeGroup>

    ```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    {
      "id": "9f2c1e7b4a...",
      "apiKey": "exo_prod_0f3a91c8d2e4b7...",
      "label": "Claude Code"
    }
    ```

    `id` is the key hash. It is what `GET /v1/keys` lists and what `DELETE /v1/keys/{id}` revokes. `apiKey` is the only thing you can paste into a client, and this response is the only place it appears.
  </Step>

  <Step title="Register the server">
    Either install the plugin:

    ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    /plugin marketplace add With-Exo-AI/exo-plugin
    /plugin install exo@exo
    ```

    Paste your key when prompted. Or register the endpoint directly, which is one command and no plugin system:

    ```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    claude mcp add exo --transport http \
      https://api.get-exo.com/mcp/ \
      --header "Authorization: Bearer exo_prod_your_key_here"
    ```

    Use one or the other, not both. They do not hard collide, because a plugin's MCP server is namespaced `plugin:exo:exo` and coexists with a standalone `exo`, but you end up with two copies of every tool. To switch, remove the other first with `claude mcp remove exo`.
  </Step>

  <Step title="Restart Claude Code">
    Claude Code reads MCP configuration at startup only. A server added mid-session does not appear until you restart.
  </Step>

  <Step title="Verify from both sides">
    Client side: `claude mcp list` shows `exo` with a check mark.

    Server side, which cannot lie: the connect panel flips to **Connected** the moment Exo receives the first authenticated request from your key, and the key list shows a **Last used** time per key. A key that still reads **Never used** after a restart was never saved or never sent. Generate a fresh one and configure it again.

    Then ask Claude Code something that references your own history, such as "what did we decide about the pricing model?", and watch it call `exo_context`.
  </Step>
</Steps>

## How the auth works

The key you paste is a long-lived, revocable Exo API key scoped to your user and organization, the same identity the dashboard uses. Claude Code sends it as `Authorization: Bearer <key>` on every MCP call. The `/mcp` endpoint validates it with the same check the REST API uses and rejects a missing, invalid or revoked key with 401.

Every validated request stamps the key's last-used time. That is the server-truth liveness signal, and it is worth more than anything your local config claims.

To rotate a key, mint the new one first, reconfigure the client, confirm the new key shows a last-used time, then revoke the old one.

## When it goes wrong

**Treat `Failed to connect` as an auth failure until proven otherwise.** Claude Code collapses a 401 into the same message it shows for an unreachable server, and the server is almost never the problem.

| Symptom                                             | Cause                                                                                                                                          | Fix                                                                                                                                        |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `Failed to connect` in `claude mcp list`            | The stored key is invalid, stale, or was never actually saved. A plugin config paste can fail silently, writing nothing and reporting nothing. | Mint a fresh key, re-enter it, restart, then check the dashboard key list. If it stays **Never used**, the client still is not sending it. |
| 401, or the tools never appear                      | Key invalid, revoked or mistyped                                                                                                               | Mint a new key and re-run the plugin install or `claude mcp add`.                                                                          |
| 401 immediately after adding a key you know is good | The URL had no trailing slash, so the redirect dropped the header                                                                              | Re-add with `/mcp/`.                                                                                                                       |
| Tools still missing after adding the server         | Claude Code only reads MCP config at startup                                                                                                   | Restart Claude Code.                                                                                                                       |
| It connects, but Exo knows nothing about you        | Your organization has no content yet                                                                                                           | [Bring data in](/integrations/imports) first.                                                                                              |

### Scripting against the endpoint directly

Two behaviors bite raw-HTTP integrators.

<AccordionGroup>
  <Accordion title="A bare POST returns 400 until you handshake" icon="circle-question">
    `/mcp` speaks MCP Streamable HTTP. Send `initialize` first, capture the `Mcp-Session-Id` response header, and send it back on every subsequent request. A POST without the session id returns 400. That is the protocol, not an outage.
  </Accordion>

  <Accordion title="Ingestion acknowledges before it finishes" icon="circle-question">
    `exo_ingest` reports success as soon as the job is durably queued. Graph counts change after the worker processes it. An unchanged count immediately after an ingest is not a failure. Poll `GET /v1/ingest/session-status`, which reports `building: true` while jobs are active, before concluding anything.
  </Accordion>
</AccordionGroup>

## Disconnecting

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
claude mcp remove exo
```

Removing the server stops the client from calling Exo. It does not invalidate the credential. Revoke the key as well, or anyone holding that string can still read your graph:

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  curl -X DELETE https://api.get-exo.com/v1/keys/9f2c1e7b4a... \
    -H "X-Exo-API-Key: $EXO_KEY"
  ```

  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  import os, requests

  requests.delete(
      "https://api.get-exo.com/v1/keys/9f2c1e7b4a...",
      headers={"X-Exo-API-Key": os.environ["EXO_KEY"]},
  )
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  await fetch("https://api.get-exo.com/v1/keys/9f2c1e7b4a...", {
    method: "DELETE",
    headers: { "X-Exo-API-Key": process.env.EXO_KEY! },
  });
  ```
</CodeGroup>

Revocation is a soft revoke and is idempotent: 204 whether or not the key was already revoked, and 404 for a key belonging to someone else.

## Next

<Columns cols={2}>
  <Card title="Coding session history" icon="terminal" href="/integrations/sessions">
    Backfill the Claude Code transcripts already on your disk, so the graph starts warm.
  </Card>

  <Card title="MCP tool reference" icon="wrench" href="/integrations/mcp-tools">
    What each tool does and the REST route behind it.
  </Card>
</Columns>
