| title | Deployment |
|---|---|
| description | Deploy a brain and connect your agent — local, remote, Docker, TLS, and the exact config to paste. |
This guide answers three questions, in order:
- Which setup do I want? — local vs remote, SQLite vs OpenSearch
- How do I run it — local, remote without Docker, remote with Docker
- How do I get the MCP details into my agent? — the part that is easy to get wrong
Two independent choices. Pick one from each column.
Where does the brain run?
| Local (stdio) | Remote (HTTP) | |
|---|---|---|
| How the agent connects | It launches open-index mcp itself |
It connects to a URL you host |
| Needs a URL or token | No | Yes |
| Who can use it | One agent, on your machine | Any agent that can reach the host |
| Use when | You're the only user; the brain lives in a repo you have checked out | A team, cloud agents, or CI shares one brain |
Which search backend?
| SQLite (default) | OpenSearch | |
|---|---|---|
| External services | None — one brain.db file |
An OpenSearch cluster |
| Concurrent writers | One. SQLite is single-writer | Many |
| Semantic search | Brute-force cosine scan, fine to ~10k entities | Native k-NN, scales past that |
| Extras | Native per-field boosting, fuzzy/typo-tolerant search | |
| Use when | Local brains, single agent, trials, read-mostly | Several 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.
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-brainopen-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.
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.
# 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 8080serve 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
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 writableA 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.
# /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.targetExecStartPre 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.
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/mcpPoint 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: trueOr leave brain.yaml on SQLite and override per-environment:
export OPEN_INDEX_SEARCH_BACKEND=opensearch
export OPEN_INDEX_OPENSEARCH_HOSTS=https://opensearch.internal:9200The 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).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 upThe brain directory is mounted, not baked in (BRAIN_DIR:/brain) — doc_types
and entities stay in your git repo. On start the entrypoint:
- fails fast with a clear message if
/brain/brain.yamlisn't there, - waits for OpenSearch to report healthy when that backend is selected,
- runs
open-index indexso file-backed entities are loaded (skip withOPEN_INDEX_SKIP_INDEX=1), - execs
open-index serve.
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-brainYou can still read and edit the files; writes from inside the container land as uid 10001 with your group.
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/mcpThis 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:
| Brains | One process | One process per brain |
|---|---|---|
| 50 | 345 MB | ~13 GB |
| 200 | 373 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.
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.
docker build -t open-index .
docker run -p 8080:8080 \
-v "$PWD/my-brain:/brain" \
-e OPEN_INDEX_TOKEN=secret \
open-index serve- Persistence. SQLite:
brain.dbis inside your mounted brain dir — back that up. OpenSearch: theopensearch-datanamed volume. Index-backed entities (storage: index) exist only there; they are not in git and are not recreated byopen-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 setsearch.username/passwordbefore putting it on a shared network. - Behind a proxy, set
OPEN_INDEX_PUBLIC_URLin.envso the printed connection details are the ones agents can actually use.
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 --cliIt writes to stdout, so it pipes straight where it belongs:
open-index mcp-config --brain ./my-brain > .mcp.json| Client | Location |
|---|---|
| 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 Desktop | claude_desktop_config.json — same mcpServers shape |
| Cursor | .cursor/mcp.json — same mcpServers shape |
| Anything else | Any 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>" }
}
}
}# 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.
| Symptom | Cause |
|---|---|
| Agent can't connect at all | You used the bind address (0.0.0.0) instead of a reachable one. Re-read the serve banner. |
401 unauthorized |
Token missing/wrong. The header must be exactly Authorization: Bearer <token>. |
Connects, but 404 |
Missing the /mcp path. mcp-config appends it; hand-written URLs often don't. |
| Connects, but every search is empty | open-index index never ran on this host, so file-backed entities aren't loaded. |
| Tools are read-only unexpectedly | The 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 brain | Relative --brain . resolved against the agent's cwd. Use an absolute path — mcp-config emits one. |
| Hangs/timeouts behind nginx | Response buffering is on. Set proxy_buffering off. |