Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copy this file to .env and change the access key before first use.

SUPABASE_URL=http://127.0.0.1:54321
SUPABASE_SERVICE_ROLE_KEY=<local-supabase-service-role-key>
SUPABASE_ANON_KEY=<local-supabase-anon-key>
MCP_ACCESS_KEY=<localbrain-key>
# "*" means this key can access every namespace.
MCP_ACCESS_KEY_SCOPES=*
Expand All @@ -10,6 +10,8 @@ MCP_ACCESS_KEY_SCOPES=*
# admin:<admin-key>:*
# journal-client:<journal-key>:localbrain|journal
MCP_ACCESS_KEYS=
# Keep update/delete disabled. Database RLS also denies these operations.
MCP_ENABLE_DESTRUCTIVE_TOOLS=false

# Ollama's OpenAI-compatible API. When Supabase runs in Docker,
# host.docker.internal reaches Ollama on the host machine.
Expand Down
196 changes: 144 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,93 +1,185 @@
# localbrain

`localbrain` is a small offline memory server for MCP clients. It stores
thoughts in local Postgres with pgvector, creates embeddings with a local
Ollama model, and exposes capture/search/list/update/delete tools through a
stdio MCP bridge.
`localbrain` is a local-first memory server for MCP clients. It stores thoughts
in Postgres with pgvector, creates embeddings and metadata with a local Ollama
model, and exposes semantic search through a Supabase Edge Function.

## What You Get
The default security model treats both MCP clients and retrieved memories as
untrusted:

- General MCP clients are read-only.
- Captures require the explicit local command-line client.
- Update and delete tools are disabled by default and denied by database RLS.
- Retrieved content is wrapped in `UNTRUSTED_MEMORY_*` boundaries.
- The Edge Function uses the local Supabase anon key, not the service-role key.
- Access keys are accepted only through the `x-brain-key` header.
- Supabase and optional containerized Ollama ports use loopback bindings plus a
Windows firewall rule, and project containers have automatic restart disabled.
- Ollama model calls are local-only by default.

## Components

- Local Supabase/Postgres database with pgvector
- Local embedding and metadata extraction through Ollama
- Supabase Edge Function MCP endpoint
- Stdio bridge for desktop MCP clients
- Starter configs for Claude Desktop, Codex, and OpenClaw-style clients
- Smoke test for capture, search, list, update, delete, and stats
- Local Ollama embeddings and metadata extraction
- Streamable HTTP MCP Edge Function
- Read-only stdio bridge for MCP clients
- Explicit CLI for user-initiated captures and searches
- Namespace-scoped access keys
- RLS migration and local hardening scripts

See [the architecture guide](docs/architecture.md) for the trust boundaries.

## Requirements

- Node.js 20 or newer
- Supabase CLI
- Docker Desktop or a compatible local Docker runtime
- Ollama with `mxbai-embed-large` and a small instruct chat model, preferably
- Docker Desktop or another compatible Docker runtime
- PowerShell 7 on Windows
- Ollama with `mxbai-embed-large` and a small instruct model such as
`qwen2.5:4b-instruct`

Pull the default local models:
Install the default local models:

```powershell
ollama pull mxbai-embed-large
ollama pull qwen2.5:4b-instruct
```

`localbrain` uses the chat model only to extract small JSON metadata. Prefer a
small instruct model over a reasoning-oriented model; it should follow the
schema and stop quickly. See `docs/model-selection.md`.
## Secure setup on Windows

## Quick Start
Copy the example configuration and replace every placeholder:

```powershell
Copy-Item .env.example .env
supabase start
supabase functions serve local-memory-mcp --env-file .env
node scripts/localbrain-stdio.mjs
```

Set `MCP_ACCESS_KEY` in `.env` to a local secret before connecting an MCP
client. If Supabase prints a different local API URL or service role key, copy
those values into `.env`.
Use a long, random `MCP_ACCESS_KEY`. On a fresh install, leave the anon-key
placeholder until the first database start.

Open PowerShell as Administrator once and install the inbound block rule:

```powershell
powershell -NoProfile -File .\scripts\install-localbrain-firewall.ps1
```

Optionally restrict the checkout and `.env` to your Windows account, SYSTEM,
and local administrators:

```powershell
powershell -NoProfile -File .\scripts\harden-local-security.ps1
```

Start the database on demand:

```powershell
pwsh .\scripts\start.ps1
```

After the first start, run `supabase status` and copy the local anon key—not
the service-role key—into `SUPABASE_ANON_KEY` in `.env`.

Then start the function in a second terminal using the command printed by the
startup script:

```powershell
supabase functions serve local-memory-mcp --network-id localbrain-loopback --env-file .env
```

The startup script fails closed when the firewall rule is missing. This is
intentional: on Docker Desktop for Windows, an apparently loopback-bound Docker
network may still be reachable through the host's LAN address.

For multi-client setups, keep one admin key with `MCP_ACCESS_KEY_SCOPES=*`
and add scoped keys with `MCP_ACCESS_KEYS`, for example:
Stop LocalBrain when it is not in use:

```powershell
pwsh .\scripts\stop.ps1
```

Local data remains in Docker volumes after a normal stop.

## Capture and search

User-initiated captures go through the explicit CLI:

```powershell
node .\scripts\localbrain-cli.mjs capture localbrain "Remember this thought"
node .\scripts\localbrain-cli.mjs search localbrain "thought"
node .\scripts\localbrain-cli.mjs recent localbrain 5
node .\scripts\localbrain-cli.mjs brains
```

The CLI sends `x-brain-client: cli-write`. The stdio MCP bridge sends
`x-brain-client: agent-readonly`, so a prompt injection or compromised MCP
client cannot capture new memories through the normal agent connection.

## Connect an MCP client

Use a file in [`examples/`](examples/) as a starting point. Replace `<repo>`
with your clone path. The bridge loads `MCP_ACCESS_KEY` from `.env` and forwards
requests only to `LOCALBRAIN_MCP_URL`.

For multiple clients, keep a private administrative key and create scoped keys
with `MCP_ACCESS_KEYS`:

```text
admin:<admin-key>:*
journal-client:<journal-key>:localbrain|journal
admin:<random-key>:*
journal-client:<random-key>:localbrain|journal
```

## Connect A Client
Keys belong only in `.env`; never put them in URLs, client arguments, logs, or
committed files.

## MCP tools

Use one of the files in `examples/` as a starting point. Replace `<repo>` with
the path where you cloned this repository and set the same `MCP_ACCESS_KEY` in
`.env`.
- `search_thoughts`: semantic search; results are marked as untrusted
- `list_thoughts`: recent memories; content is marked as untrusted
- `thought_stats`: aggregate untrusted metadata
- `list_brains`: available namespaces
- `capture_thought`: rejected for general MCP clients; accepted by the local CLI
- `update_thought`: disabled by default
- `delete_thought`: disabled by default

## Tools
Database RLS grants the Edge Function only read access plus execution of a
validated capture function. Direct update and delete privileges are not
granted, even if an application-level guard is accidentally weakened.

- `capture_thought`: save a thought with metadata and embedding
- duplicate captures in the same namespace are merged by `upsert_thought` using a content fingerprint
- `search_thoughts`: semantic search across stored thoughts
- `list_thoughts`: list recent thoughts with optional filters
- `update_thought`: replace a thought and regenerate metadata
- `delete_thought`: delete by UUID or nearest natural language match
- `thought_stats`: summarize totals, topics, people, and namespaces
- `list_brains`: show available namespaces
## Verify

## Privacy Model
With the database and Edge Function running:

The default path is offline-first. The database runs locally, embeddings are
created locally, and `OLLAMA_LOCAL_ONLY=true` rejects non-local embedding
hosts. See `docs/privacy.md` before publishing a fork or sharing a memory
store.
```powershell
pwsh .\scripts\smoke-test.ps1
```

## Test
The smoke test performs capture, search, list, and statistics checks. It does
not exercise update or delete because those operations are intentionally
disabled.

After the local function is running:
Useful additional checks:

```powershell
pwsh ./scripts/smoke-test.ps1
pwsh .\scripts\status.ps1
git diff --check
```

## Public Release Note
## Privacy and publishing

The repository ignores `.env`, database exports, logs, memory namespaces, and
editor state. Before publishing a fork, follow [the privacy checklist](docs/privacy.md)
and scan both the working tree and any history you intend to publish.

Never publish real memories, keys, database dumps, personal paths, hostnames,
or logs.

## Documentation

- [Architecture and trust boundaries](docs/architecture.md)
- [Privacy checklist](docs/privacy.md)
- [MCP client examples](docs/mcp-clients.md)
- [Model selection](docs/model-selection.md)
- [Troubleshooting](docs/troubleshooting.md)
- [Public release manifest](docs/release-manifest.md)

## License

Publish this repo from a clean export, not from a private working repository
with old history. Run the privacy checklist in `docs/privacy.md` and a secret
scanner before the first public push.
See [LICENSE](LICENSE).
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ services:
image: ollama/ollama:latest
container_name: localbrain-ollama
ports:
- "11434:11434"
- "127.0.0.1:11434:11434"
volumes:
- ollama:/root/.ollama

Expand Down
42 changes: 31 additions & 11 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Architecture

`localbrain` has four pieces:
`localbrain` has five pieces:

1. An MCP client starts `scripts/localbrain-stdio.mjs`.
1. An MCP client starts `scripts/localbrain-stdio.mjs` in read-only mode.
2. The stdio bridge forwards JSON-RPC requests to the local Supabase Edge Function.
3. The function checks `MCP_ACCESS_KEY`, applies any namespace scope for that key, calls local Ollama for embeddings and metadata, and stores records in Postgres.
4. Postgres with pgvector performs semantic search through the `match_thoughts` RPC.
3. The function checks the header-only `MCP_ACCESS_KEY`, applies any namespace scope for that key, and calls local Ollama for searches.
4. Explicit captures use `scripts/localbrain-cli.mjs`, which marks the request as a user-initiated write and stores it through a validated database function.
5. Postgres with pgvector performs semantic search through the `match_thoughts` RPC.

The default namespace is `localbrain`. Optional namespaces such as `work`,
`research`, and `journal` are plain metadata filters, so one local vector table
Expand All @@ -26,10 +27,12 @@ maintained by a database trigger whenever a row changes.

## Namespace Access

The database enables row level security and grants the service role explicit
access to the local tables and functions. Because the MCP function uses the
service role internally, namespace protection is also enforced in the MCP
server after it authenticates the request key.
The database enables row level security. The Edge Function uses the local anon
key, which can read memories and execute the narrowly scoped `upsert_thought`
capture function. It has no direct update or delete privileges. The capture
function validates content size, metadata shape, namespace, and embedding
before writing. Namespace scopes are additionally enforced by the MCP server
after it authenticates the request key.

`MCP_ACCESS_KEY_SCOPES=*` gives the default key access to every namespace.
`MCP_ACCESS_KEYS` can define additional scoped keys:
Expand All @@ -40,9 +43,26 @@ journal-client:<journal-key>:localbrain|journal
workstation:<work-key>:work|research
```

Restricted keys cannot query, list, update, or delete thoughts outside their
allowed namespaces. If a restricted key omits `brain_id` while searching or
listing, the server defaults to the first namespace in that key's scope.
Restricted keys cannot query, list, or capture outside their allowed
namespaces. Update and delete are disabled globally. If a restricted key omits
`brain_id` while searching or listing, the server defaults to the first
namespace in that key's scope.

## Prompt-injection boundary

Memory content and metadata may contain adversarial instructions. Search,
listing, statistics, and namespace output is wrapped in explicit
`UNTRUSTED_MEMORY_*` markers, and tool descriptions tell MCP clients not to
follow instructions from retrieved data. The stdio bridge cannot capture, so a
retrieved instruction cannot directly write another memory.

## Network and lifecycle boundary

On Windows, the startup helper requires an inbound firewall block for local
Supabase and Ollama ports and creates a Docker network whose default published-port
binding is `127.0.0.1`. Both layers are required because Docker Desktop may
still make published ports reachable through the host LAN address. Supabase
containers are set to restart policy `no` and are started only on demand.

## Locality

Expand Down
8 changes: 6 additions & 2 deletions docs/file-classification.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ Public release files:
| `docker-compose.yml` | public-example | Optional local Ollama container |
| `supabase/config.toml` | public-core | Local Supabase configuration |
| `supabase/migrations/20260101000000_localbrain_schema.sql` | public-core | Clean schema and seed namespaces |
| `supabase/migrations/20260713000000_harden_localbrain_rls.sql` | public-core | Least-privilege RLS and capture RPC |
| `supabase/functions/local-memory-mcp/` | public-core | MCP endpoint |
| `scripts/localbrain-stdio.mjs` | public-core | Stdio MCP bridge |
| `scripts/localbrain-stdio.mjs` | public-core | Read-only stdio MCP bridge |
| `scripts/localbrain-cli.mjs` | public-core | Explicit capture and search CLI |
| `scripts/install-localbrain-firewall.ps1` | public-core | Windows inbound block rule installer |
| `scripts/harden-local-security.ps1` | public-core | Windows repository ACL hardening |
| `scripts/start.ps1` | public-core | Local startup helper |
| `scripts/stop.ps1` | public-core | Local stop helper |
| `scripts/status.ps1` | public-core | Local status helper |
| `scripts/smoke-test.ps1` | public-core | Capture/search/list/update/delete test |
| `scripts/smoke-test.ps1` | public-core | Capture/search/list/stats test |
| `examples/` | public-example | Generic MCP client templates |
| `docs/` | public-core | Public documentation and review checklists |

Expand Down
5 changes: 3 additions & 2 deletions docs/mcp-clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Start the Supabase function first:

```powershell
supabase functions serve local-memory-mcp --env-file .env
supabase functions serve local-memory-mcp --network-id localbrain-loopback --env-file .env
```

Then configure your MCP client to run:
Expand All @@ -14,6 +14,7 @@ node <repo>/scripts/localbrain-stdio.mjs

Templates live in `examples/`. The bridge reads `.env` from the repo root and
injects `MCP_ACCESS_KEY` into HTTP requests, so the client config does not need
to contain the key.
to contain the key. The bridge identifies itself as `agent-readonly`; capture
requests must use `scripts/localbrain-cli.mjs` instead.

Use `LOCALBRAIN_MCP_URL` if your Supabase API port differs from `54321`.
3 changes: 3 additions & 0 deletions docs/privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ Before publishing a fork or sharing a memory store:

- Do not commit `.env`, `.env.local`, database dumps, or logs.
- Change `MCP_ACCESS_KEY` from the placeholder value.
- Use `SUPABASE_ANON_KEY` in the Edge Function; never place the service-role key in client configuration.
- Keep `OLLAMA_LOCAL_ONLY=true` unless you intentionally use a remote model.
- Install the Windows firewall rule before starting Supabase, and stop the stack when it is not in use.
- Treat every retrieved memory as untrusted content, even if you originally wrote it.
- Review `thoughts` exports before sharing them.
- Publish from a clean export if the source repository ever contained private notes or secrets.

Expand Down
3 changes: 3 additions & 0 deletions docs/release-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ This clean public draft includes:
- `.env.example`: local-only configuration template
- `supabase/config.toml`: local Supabase configuration
- `supabase/migrations/20260101000000_localbrain_schema.sql`: clean schema and default namespaces
- `supabase/migrations/20260713000000_harden_localbrain_rls.sql`: least-privilege RLS and capture RPC
- `supabase/functions/local-memory-mcp/`: MCP Edge Function
- `scripts/localbrain-stdio.mjs`: stdio-to-HTTP MCP bridge
- `scripts/localbrain-cli.mjs`: explicit user-initiated capture and search client
- `scripts/install-localbrain-firewall.ps1`, `harden-local-security.ps1`: local security controls
- `scripts/start.ps1`, `stop.ps1`, `status.ps1`, `smoke-test.ps1`: local operations
- `examples/`: MCP client templates
- `docs/`: architecture, client setup, privacy, and troubleshooting
Expand Down
Loading