deployment.mdx

August 10, 2026 · View on GitHub

This guide answers three questions, in order:

  1. Which setup do I want? — local vs remote, SQLite vs OpenSearch
  2. How do I run it — local, remote without Docker, remote with Docker
  3. How do I get the MCP details into my agent? — the part that is easy to get wrong
There is a second, unrelated use of "MCP" in Open Index — *connectors*, which pull data **from** someone else's MCP server into your brain. That's [Connectors](/agents/connectors).

1. Which setup do I want?

Two independent choices. Pick one from each column.

Where does the brain run?

Local (stdio)Remote (HTTP)
How the agent connectsIt launches open-index mcp itselfIt connects to a URL you host
Needs a URL or tokenNoYes
Who can use itOne agent, on your machineAny agent that can reach the host
Use whenYou're the only user; the brain lives in a repo you have checked outA team, cloud agents, or CI shares one brain

Which search backend?

SQLite (default)OpenSearch
External servicesNone — one brain.db fileAn OpenSearch cluster
Concurrent writersOne. SQLite is single-writerMany
Semantic searchBrute-force cosine scan, fine to ~10k entitiesNative k-NN, scales past that
ExtrasNative per-field boosting, fuzzy/typo-tolerant search
Use whenLocal brains, single agent, trials, read-mostlySeveral agents writing, or >10k entities

Rule of thumb: local + SQLite to start. Move to remote + OpenSearch when a second writer appears — that's the line SQLite can't cross, not entity count.

**OpenSearch is only set up for you in the Docker path.** Running it outside Docker means operating a cluster yourself; Open Index will happily connect to one ([config below](#opensearch-without-docker)), but this repo only ships a ready-made cluster in `docker-compose.yml`.

Switching backends does not require editing brain.yaml — set OPEN_INDEX_SEARCH_BACKEND=sqlite|opensearch in the environment and it wins over the file. That's what the compose profiles do.

2. Local brain (stdio)

Nothing to host. The agent starts the server itself over stdio.

pip install -e '.[all]'
open-index init my-brain
open-index index --brain my-brain

open-index init already writes a .mcp.json into the brain directory, so if you open Claude Code in that folder, it connects automatically — no further setup.

To connect from somewhere else (a different repo, Claude Desktop, Cursor), get the config block:

open-index mcp-config --brain ./my-brain
{
  "mcpServers": {
    "open-index": {
      "command": "open-index",
      "args": ["mcp", "--brain", "/absolute/path/to/my-brain"]
    }
  }
}

The path is absolutized deliberately: your agent's working directory is usually not the brain directory, and a relative --brain . quietly opens the wrong place (or an empty one). See Connecting your agent for where this block goes.

3. Remote brain, without Docker

Use this when you have a VM and don't want containers. The brain becomes an HTTP MCP endpoint that any agent can register by URL.

Install and run

# On the host
pip install 'open-index[serve]'                 # add ,opensearch if using OpenSearch
git clone <your-brain-repo> /srv/acme-brain     # your doc_types + entities
open-index index --brain /srv/acme-brain        # load file-backed entities

export OPEN_INDEX_TOKEN=$(openssl rand -hex 32)
open-index serve --brain /srv/acme-brain --port 8080

serve prints exactly what to connect to:

open-index · brain 'acme' · read+write · search backend: sqlite
  listening on 0.0.0.0:8080

  on this machine        http://127.0.0.1:8080/mcp
  from another machine   http://10.0.1.42:8080/mcp

  auth: Authorization: Bearer 9f3a…******
  agent config:  open-index mcp-config --url http://10.0.1.42:8080 --token $OPEN_INDEX_TOKEN
**`0.0.0.0` is not an address.** It's the *bind* address — "listen on every interface". It is never what you paste into an agent. That's why the banner prints the reachable addresses separately.

The token is not optional

serve exposes put_entity and create_doc_type. Without a token, anyone who can reach the port can rewrite your brain. Either set OPEN_INDEX_TOKEN, or pass --read-only to drop the write tools:

open-index serve --brain /srv/acme-brain --read-only          # queryable, not writable

A common shape is two endpoints: a read-only one on an open port, and an authenticated read+write one for the agents allowed to author.

Keep it running (systemd)

# /etc/systemd/system/open-index.service
[Unit]
Description=open-index brain (MCP)
After=network-online.target

[Service]
User=openindex
WorkingDirectory=/srv/acme-brain
Environment=OPEN_INDEX_TOKEN=<token>
# Uncomment to use an OpenSearch cluster instead of SQLite:
# Environment=OPEN_INDEX_SEARCH_BACKEND=opensearch
# Environment=OPEN_INDEX_OPENSEARCH_HOSTS=https://opensearch.internal:9200
ExecStartPre=/usr/local/bin/open-index index --brain /srv/acme-brain
ExecStart=/usr/local/bin/open-index serve --brain /srv/acme-brain --port 8080
Restart=on-failure

[Install]
WantedBy=multi-user.target

ExecStartPre matters: file-backed entities live in git, not in the index. After a git pull or a fresh machine, the index is empty until open-index index runs and the brain answers every query with nothing.

TLS / behind a proxy

serve speaks plain HTTP. For TLS, terminate at nginx/Caddy and forward to it. Streamable HTTP uses long-lived responses, so disable response buffering:

location /mcp {
    proxy_pass http://127.0.0.1:8080/mcp;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Authorization $http_authorization;
    proxy_buffering off;          # required — SSE/streaming responses
    proxy_read_timeout 3600s;
}

Then tell serve its public name so the banner and mcp-config print the URL agents should actually use, rather than the internal one:

open-index serve --brain /srv/acme-brain --public-url https://brain.acme.com/mcp

OpenSearch without Docker

Point brain.yaml at your existing cluster. Secrets stay as ${ENV} refs, resolved when the connection is opened:

search:
  backend: opensearch
  hosts: ["https://opensearch.internal:9200"]
  index: open_index_acme          # optional; defaults to open_index_<brain name>
  username: "${OPENSEARCH_USER}"
  password: "${OPENSEARCH_PASSWORD}"
  use_ssl: true
  verify_certs: true

Or leave brain.yaml on SQLite and override per-environment:

export OPEN_INDEX_SEARCH_BACKEND=opensearch
export OPEN_INDEX_OPENSEARCH_HOSTS=https://opensearch.internal:9200

4. Remote brain, with Docker

The shortest path to a shared brain, and the only path where OpenSearch is set up for you.

cp .env.example .env
# Set OPEN_INDEX_TOKEN (openssl rand -hex 32) and BRAIN_DIR (path to your brain).
No external services: ```bash docker compose --profile sqlite up --build ``` Adds a single-node cluster with a persistent volume: ```bash docker compose --profile opensearch up --build ```

Both serve http://localhost:8080/mcp. The only difference is OPEN_INDEX_SEARCH_BACKEND; your brain.yaml is identical either way, so you can switch by changing the profile and nothing else.

Add the explorer UI on :8501 alongside either:

docker compose --profile opensearch --profile ui up

What the container does on start

The brain directory is mounted, not baked in (BRAIN_DIR:/brain) — doc_types and entities stay in your git repo. On start the entrypoint:

  1. fails fast with a clear message if /brain/brain.yaml isn't there,
  2. waits for OpenSearch to report healthy when that backend is selected,
  3. runs open-index index so file-backed entities are loaded (skip with OPEN_INDEX_SKIP_INDEX=1),
  4. execs open-index serve.

Permissions on the mounted brain directory

The container runs as uid 10001 (not root), so a bind-mounted brain directory owned by your user is not writable by it — indexing fails on the first write. Give the container ownership and keep group access for yourself:

sudo chown -R 10001:"$(id -g)" /path/to/my-brain
chmod -R g+rwX /path/to/my-brain

You can still read and edit the files; writes from inside the container land as uid 10001 with your group.

Many brains from one process

serve --brain <dir> runs one brain. For more than a handful, --brains <root> serves every brain under a directory from a single process, each at /<name>/mcp:

open-index serve --brains /srv/brains --port 8080
#   /srv/brains/support/  →  /support/mcp
#   /srv/brains/sales/    →  /sales/mcp

This matters because of what is not duplicated. A process per brain re-loads the Python runtime and a ~250MB resident embedding model each time, so a modest host tops out at a handful. In one process the model is loaded once and a brain costs only its config and doc_types.

Measured on the bundled example brain, SQLite-backed:

BrainsOne processOne process per brain
50345 MB~13 GB
200373 MB~53 GB

That is 1.8MB of marginal cost per brain, and 200 mount in ~4 seconds.

Each brain keeps its own storage, its own read/write policy and its own token — OPEN_INDEX_TOKEN_<NAME> gates one brain (OPEN_INDEX_TOKEN_SALES_EU for sales-eu/), and --token covers any without one. Nothing is shared between brains except the process and the model.

Two extras come with it: GET / lists every brain with its URL, entity count and doc_types, and GET /healthz is an unauthenticated probe for a load balancer.

**Prefer SQLite here.** With OpenSearch, every brain is a separate cluster index and therefore a shard; the working guidance is ~20 shards per GB of heap, so hundreds of brains would hit that ceiling long before RAM. SQLite gives each brain its own file and no shard cost at all. Use OpenSearch for the few brains that genuinely need concurrent writers or >10k entities.

Running one-off commands

docker compose --profile sqlite run --rm brain-sqlite validate
docker compose --profile sqlite run --rm brain-sqlite search "checkout latency"
docker compose --profile sqlite run --rm brain-sqlite ingest my-connector

--brain /brain is added for you.

Plain docker run, no compose

docker build -t open-index .
docker run -p 8080:8080 \
  -v "$PWD/my-brain:/brain" \
  -e OPEN_INDEX_TOKEN=secret \
  open-index serve

Notes for real deployments

  • Persistence. SQLite: brain.db is inside your mounted brain dir — back that up. OpenSearch: the opensearch-data named volume. Index-backed entities (storage: index) exist only there; they are not in git and are not recreated by open-index index. Back it up or be able to re-ingest.
  • The OpenSearch cluster here has security disabled (DISABLE_SECURITY_PLUGIN=true) and binds to loopback. That's fine for a single host where only the brain container talks to it; enable the security plugin and set search.username/password before putting it on a shared network.
  • Behind a proxy, set OPEN_INDEX_PUBLIC_URL in .env so the printed connection details are the ones agents can actually use.

5. Connecting your agent

You need two things: the URL (remote) or brain path (local), and the token (remote only). mcp-config assembles both into the right block.

# Local brain
open-index mcp-config --brain ./my-brain

# Remote brain — host:port, or a full URL; /mcp is appended if you omit it
open-index mcp-config --url brain.acme.internal:8080 --token $OPEN_INDEX_TOKEN

# As a `claude mcp add` one-liner instead of JSON
open-index mcp-config --url https://brain.acme.com/mcp --token $TOKEN --cli

It writes to stdout, so it pipes straight where it belongs:

open-index mcp-config --brain ./my-brain > .mcp.json

Where the block goes

ClientLocation
Claude Code (project).mcp.json in the repo root — shared with the team via git
Claude Code (user-wide)claude mcp add … (use --cli to get the exact command)
Claude Desktopclaude_desktop_config.json — same mcpServers shape
Cursor.cursor/mcp.json — same mcpServers shape
Anything elseAny MCP client that speaks stdio or streamable HTTP

A remote block looks like this:

{
  "mcpServers": {
    "open-index": {
      "type": "http",
      "url": "https://brain.acme.com/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}
Committing a token into `.mcp.json` puts it in git history. For a shared repo, prefer a per-developer user-scoped entry, or a read-only endpoint with no token for the committed config.

Check it works before blaming the agent

# 401 = the server is up and your token is wrong or missing.
# 406/400 = you reached the MCP endpoint (it wants a proper MCP handshake). Good.
curl -i -X POST https://brain.acme.com/mcp \
  -H "Authorization: Bearer $OPEN_INDEX_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'

Once connected, the agent should call navigation_guidelines() first — it reports the doc_types, fields, and relationship vocabulary of your brain.

Troubleshooting

SymptomCause
Agent can't connect at allYou used the bind address (0.0.0.0) instead of a reachable one. Re-read the serve banner.
401 unauthorizedToken missing/wrong. The header must be exactly Authorization: Bearer <token>.
Connects, but 404Missing the /mcp path. mcp-config appends it; hand-written URLs often don't.
Connects, but every search is emptyopen-index index never ran on this host, so file-backed entities aren't loaded.
Tools are read-only unexpectedlyThe server was started with --read-only.
Writes fail with "unknown doc_type"Create the doc_type first (create_doc_type), or check navigation_guidelines() for existing ones.
Local stdio server opens an empty brainRelative --brain . resolved against the agent's cwd. Use an absolute path — mcp-config emits one.
Hangs/timeouts behind nginxResponse buffering is on. Set proxy_buffering off.