Memory Vault
Memory serverOfficial Mihai Builds project
Created by MihaiBuilds
Published in MihaiBuilds/memory-vault. Publisher page
67 GitHub stars
What it is
A local-first AI memory system with hybrid search, MCP integration, and a knowledge graph.
AutoGineer overview, from the brain’s own files
Memory Vault provides a persistent storage layer for AI agents, solving the problem of context loss between sessions. It uses a single PostgreSQL database with pgvector to store memories, decisions, and project context. By combining hybrid semantic and keyword search with an automated knowledge graph, it allows agents to recall information accurately. It is designed to be platform-agnostic, exposing its capabilities through the Model Context Protocol (MCP) for integration with tools like Claude, as well as a REST API for custom applications. The system includes a web dashboard for managing memories and chatting with data using local LLMs.
How it works
Memory model: Hybrid
AutoGineer overview, from the brain’s own files
The system runs as a memory server using PostgreSQL and pgvector for storage. It processes data through a hybrid search engine that combines vector embeddings and keyword matching. An automated entity extraction process using spaCy builds a knowledge graph. The system exposes these capabilities via an MCP server for agent communication, a REST API for external integration, and a web-based dashboard for manual interaction and local LLM-powered chat.
| Folder | Role | Files |
|---|---|---|
| .github | agent-config | 12 |
| docs | docs | 5 |
| scripts | scripts | 2 |
| src | source-code | 49 |
| tests | tests | 57 |
| web | folder | 28 |
The agent writes
- docker-compose.yml evidence: README.md
Requirements
Apps: Docker
Runtimes: Python >=3.11
Databases: PostgreSQL, Redis
API keys (by name): MV_TOKEN
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Google Antigravity, Kiro, OpenCode, Amp, Roo Code, Cline, JetBrains Junie, Devin (ex-Windsurf)
Set it up
- Not scanned by AutoGineerThe author’s README also describes installing it with Docker Compose. Read its install section: AutoGineer did not scan that path. https://github.com/MihaiBuilds/memory-vault/blob/19d10c58319c63244f986ffa5bc6705f90e15d83/README.md#can-i-run-multiple-memory-vault-instances
- Not scanned by AutoGineerThe author’s README also describes installing it with Docker Compose. Read its install section: AutoGineer did not scan that path. https://github.com/MihaiBuilds/memory-vault/blob/19d10c58319c63244f986ffa5bc6705f90e15d83/README.md#quick-start-docker
- Not scanned by AutoGineerThe author’s README also describes installing it from a clone of its repository. Read its install section: AutoGineer did not scan that path. https://github.com/MihaiBuilds/memory-vault/blob/19d10c58319c63244f986ffa5bc6705f90e15d83/README.md#quick-start-docker
- Not scanned by AutoGineerThe author’s README also describes installing it from a clone of its repository. Read its install section: AutoGineer did not scan that path. https://github.com/MihaiBuilds/memory-vault/blob/19d10c58319c63244f986ffa5bc6705f90e15d83/README.md#setup-with-pip--venv-fallback
- Not scanned by AutoGineerThe author’s README also describes installing it from a clone of its repository. Read its install section: AutoGineer did not scan that path. https://github.com/MihaiBuilds/memory-vault/blob/19d10c58319c63244f986ffa5bc6705f90e15d83/README.md#setup-with-uv-recommended
- Not scanned by AutoGineerThe author also offers a container image (ghcr.io/mihaibuilds/memory-vault-mcp:1.6.1). https://github.com/MihaiBuilds/memory-vault/tree/19d10c58319c63244f986ffa5bc6705f90e15d83
- Not scanned by AutoGineerIts dependencies are installed by the package manager when you run it and were not scanned by AutoGineer (12 declared). https://github.com/MihaiBuilds/memory-vault/tree/19d10c58319c63244f986ffa5bc6705f90e15d83
Created by MihaiBuilds. Published in MihaiBuilds/memory-vault. Licence: MIT License.
Usage
AutoGineer overview, from the brain’s own files
- Integrate with Claude Desktop or Claude Code via MCP
- Query memories through a local LLM chat interface
- Build custom AI tools using the REST API
- Manage and browse memories via the web dashboard
- Ingest documentation or notes via CLI
- “Recall everything about hybrid search.”
- The agent retrieves relevant memory chunks using the hybrid search engine and provides a grounded answer.
- “Remember that we chose Redis for the session cache.”
- The agent stores this decision as a new memory, which is then auto-classified and embedded for future retrieval.
Documentation by MihaiBuilds
Memory Vault
Image: Tests Image: License: MIT Image: Python 3.11+ Image: Latest Release Image: Docker Image Image: MCP Registry Image: Ruff
The memory database for AI applications. Self-hosted Postgres + pgvector with hybrid search, MCP-native, and a knowledge graph baked in.
Every conversation with Claude or ChatGPT starts from zero. No memory of what you built last week, what decisions you made last month, what problems you've already solved. You either re-explain everything from scratch, or paste in a wall of context and hope it fits in the window.
Memory Vault is the persistent layer underneath. It stores what you want your AI to remember — decisions, conversations, notes, project context — in a single Postgres database with hybrid semantic + keyword search. Claude can recall and store memories during any session via MCP, you can chat with your own memories through a local LLM, or you can build your own AI tool on top of the REST API.
Image: Memory Vault chat with sources
Chat with your vault using a local LLM. Every answer shows the exact memories it was grounded in — click any source to verify.
Status
v1.0 — released 2026-05-07. First stable release of Memory Vault. M1-M7 (hybrid search, Docker, MCP, REST API, dashboard, knowledge graph, local LLM chat) all shipped and stable.
See CHANGELOG.md for the full per-release history, or GitHub Releases for the platform-native view.
Semver from here forward — the public surface (REST API endpoints, MCP tool signatures, DB schema) is stable. Breaking changes only on a major version bump.
Quick Start (Docker)
git clone https://github.com/MihaiBuilds/memory-vault.git
cd memory-vault
docker compose up -dThat's it. PostgreSQL + pgvector + Memory Vault, running and ready. Migrations run automatically on first start.
# Check it's working
docker compose exec app memory-vault status
# Ingest a file
docker compose exec app memory-vault ingest /path/to/file.md --space default
# Search
docker compose exec app memory-vault search "your query here"Data persists in a Docker volume — docker compose down and up again, your memories are still there.
Open http://localhost:8000 in your browser to use the dashboard (Chat, Search, Browse, Graph, Ingest, Stats).
Windows users: clone into WSL2, not a Windows path, and read docs/windows.md if you hit a line-ending error.
No-Docker quick start
If you prefer running without Docker:
Prerequisites
Setup with uv (recommended)
# Clone
git clone https://github.com/MihaiBuilds/memory-vault.git
cd memory-vault
# Create virtual environment and install dependencies
uv sync
# Install the spaCy language model
uv run python -m spacy download en_core_web_sm
# Configure
cp .env.example .env
# Edit .env with your PostgreSQL credentials
# Run migrations
uv run memory-vault migrate
# Verify
uv run memory-vault statusSetup with pip + venv (fallback)
If you don't want to install uv, plain pip + venv works too:
# Clone
git clone https://github.com/MihaiBuilds/memory-vault.git
cd memory-vault
# Create virtual environment
python -m venv .venv
source .venv/bin/activate
# Install dependencies
pip install -e .
# Install the spaCy language model
python -m spacy download en_core_web_sm
# Configure
cp .env.example .env
# Edit .env with your PostgreSQL credentials
# Run migrations
memory-vault migrate
# Verify
memory-vault statusUsage
If you set up with uv, prefix commands with uv run (e.g. uv run memory-vault search ...). If you used pip + venv, activate the venv first (source .venv/bin/activate) and run commands directly:
# Ingest a file
memory-vault ingest notes.md --space default
# Search memories
memory-vault search "hybrid search architecture" --limit 5
# Check status
memory-vault statusFeatures
Hybrid search — semantic similarity + keyword matching combined, so you find the right memory even when you don't remember the exact words
MCP integration — four tools (
recall,remember,forget,memory_status) that Claude can use natively during any sessionLocal LLM chat — query your own memories through LM Studio without sending anything to the cloud, with sources shown for every answer
Knowledge graph — entities and relationships extracted automatically, connections between things emerge over time
Memory spaces — separate namespaces for different projects or domains
REST API — integrate AI memory into any application
One-command setup —
docker compose upand it's runningSelf-hosted — your data stays on your machine, always
Architecture
Image: Memory Vault architecture
Postgres + pgvector at the core. The same memory layer is reachable from MCP (Claude), the dashboard chat page, the REST API, and any app you build on top.
Three things are deliberate about this stack:
One database, not two. Vector embeddings, full-text indexes, and relational data all live in Postgres. No separate vector DB to keep in sync.
Frontend-agnostic. The dashboard is one consumer of the API, not the API itself. MCP, REST, CLI, and your own apps are equal first-class clients.
CPU-only by default. No GPU required. Embeddings (sentence-transformers) and entity extraction (spaCy) both run on a normal laptop.
Tech Stack
PostgreSQL 16 + pgvector — vector storage and hybrid search in one database
Python 3.11+ — async backend with psycopg 3
sentence-transformers —
all-MiniLM-L6-v2embeddings (384-d, runs on CPU)spaCy —
en_core_web_smfor entity extraction (CPU-only, no LLM calls)FastAPI — REST API with bearer auth, rate limiting, and OpenAPI docs
React 19 + Vite + TanStack Query — web dashboard, baked into the main Docker image
Cytoscape.js + cose-bilkent — force-directed knowledge graph rendering on the dashboard
Docker — one-command deployment with
docker compose upMCP — Claude integration via FastMCP (stdio transport)
MCP Integration (Claude Desktop & Claude Code)
Memory Vault exposes six tools via the Model Context Protocol so Claude can read and write memories during any conversation.
Tools
| Tool | Description |
|---|---|
recall | Search memories with hybrid search (vector + full-text + RRF) |
remember | Store a new memory — auto-classified and embedded |
forget | Soft-delete a memory by chunk ID |
purge_forgotten | Permanently delete memories forgotten more than N days ago |
move_memory | Move a memory to another space, rebuilding its graph entries |
memory_status | Database health, chunk counts, embedding model info |
Resources
| Resource | Description |
|---|---|
memory://spaces | List all memory spaces with chunk counts |
memory://stats | Current system statistics |
Setup — Claude Code
Make sure you've completed the No-Docker quick start above (uv sync or pip install, plus the spaCy model download) so the memory_vault package is installed.
Project scope — add to your project's .mcp.json:
{
"mcpServers": {
"memory-vault": {
"command": "/path/to/memory-vault/.venv/bin/python",
"args": ["-m", "memory_vault.mcp"],
"env": {
"DB_HOST": "localhost",
"DB_PORT": "5432",
"DB_NAME": "memory_vault",
"DB_USER": "memory_vault",
"DB_PASSWORD": "memory_vault"
}
}
}
}Global scope — to make memory-vault available in every Claude Code session, add the same server block to ~/.claude/.mcp.json, then add memory-vault to enabledMcpjsonServers in ~/.claude/settings.json:
{
"enabledMcpjsonServers": ["memory-vault"]
}Verify with claude mcp list — memory-vault should show connected.
Setup — Claude Desktop
Add the same server block to Claude Desktop's config (Settings → Developer → Edit Config), then restart Claude Desktop.
Docker Users
If you're running Memory Vault via Docker, use DB_HOST: "127.0.0.1" and make sure port 5432 is exposed in your docker-compose.yml:
{
"mcpServers": {
"memory-vault": {
"command": "/path/to/memory-vault/.venv/bin/python",
"args": ["-m", "memory_vault.mcp"],
"env": {
"DB_HOST": "127.0.0.1",
"DB_PORT": "5432",
"DB_NAME": "memory_vault",
"DB_USER": "memory_vault",
"DB_PASSWORD": "memory_vault"
}
}
}
}The MCP server runs on the host (not inside Docker) and connects to the PostgreSQL container over the exposed port.
Verify It Works
Once configured, Claude will have access to the memory tools. Try:
"Use memory_status to check the memory system."
"Remember that we chose Redis for the session cache."
"Recall everything about hybrid search."
Troubleshooting
ModuleNotFoundErroron startup — the virtual environment isn't installed; re-runuv sync(orpip install -e .) in the repo directory.OSError: [E050]on startup — the spaCy language model isn't installed; re-runpython -m spacy download en_core_web_sm.Server shows
failedin Claude Code — runclaude --debug mcpto see the server's error output.Tools not available in Claude Code despite server connecting — confirm
memory-vaultis listed inenabledMcpjsonServersin~/.claude/settings.json.Connection refused with Docker running — use
DB_HOST: "127.0.0.1"instead of"localhost".
Remote access over HTTP
Everything above runs MCP over stdio, which assumes the client and the server are on the same machine. If your client is somewhere else — Claude Desktop on another machine, an agent harness in a different container — Memory Vault can also serve MCP over HTTP using SSE.
It is off by default. A memory store is not something to start listening on a port without being asked:
MCP_HTTP_ENABLED=trueThe transport then mounts at /api/mcp on the same port as the REST API, so a
client connects to http://<host>:8000/api/mcp/sse.
Every request needs a token, created the same way as for the REST API:
memory-vault token create my-remote-clientClients differ in how they express a remote MCP server, but they all need the same three things — the SSE transport, the URL, and the token. In the shape most of them use:
{
"mcpServers": {
"memory-vault": {
"type": "sse",
"url": "http://192.168.1.50:8000/api/mcp/sse",
"headers": { "Authorization": "Bearer mv_your-token-here" }
}
}
}Say which hostnames are yours
Memory Vault answers only to hostnames you have declared, and refuses anything
else with HTTP 421. That check is what stops a page in a browser resolving
its own domain to 127.0.0.1 and reaching your memory store through your
machine.
The default covers a client on the same machine (localhost and 127.0.0.1,
on any port). Reaching it by any other name — a LAN address, a container name,
a hostname behind a reverse proxy — means saying so:
MCP_HTTP_ALLOWED_HOSTS=memory.internal:8000,192.168.1.50:8000The list replaces the default rather than adding to it, so include
localhost:* if you still want local clients. A host:* entry matches that
host on any port.
If a client gets 421 Misdirected Request, this is why: the name it used is
not on the list.
Security
API_AUTH_ENABLED=false does not open the MCP transport. That setting is a
local-development convenience for the REST API, and it deliberately stops
there — an unauthenticated REST API on your own laptop is a convenience, while
an unauthenticated memory store on a listening port is how instances end up
public. With the flag off, /api/spaces answers without a token and
/api/mcp/sse still answers 401.
Two things worth knowing before you expose the port:
Bind to somewhere sensible. Publishing the container port on
0.0.0.0reaches the whole network. Bind to a LAN address or keep it behind a reverse proxy or tunnel.There is no transport encryption. Tokens travel in a header, so put TLS in front of it — a reverse proxy or a tunnel — for anything crossing a network you do not control.
Tokens behave exactly as they do for the REST API: revoke one with
memory-vault token revoke <prefix> and it stops working on both surfaces at
once, because both check the same table.
stdio keeps working unchanged. This is an additional way in, not a replacement, and enabling it changes nothing for existing clients.
Local LLM Chat
Memory Vault includes a chat page that lets you talk to your own memories using a local LLM — no cloud, no OpenAI key, no telemetry. The dashboard runs hybrid search against your vault, builds a context block from the top hits, and streams the answer back from a model running on your machine.
Sources are shown with every answer. Every response includes the exact chunks the LLM used, with similarity scores and content previews. Click any source to verify the answer is grounded in your data, not invented. This is the differentiator vs. opaque chat-over-docs tools — you always know what the model saw.
Setup
Memory Vault uses LM Studio as the local LLM provider in v1.0.
Download and install LM Studio.
Load a non-thinking model — Qwen2.5 (7B+), Llama 3 (8B+), or similar. Avoid Qwen3, DeepSeek-R1, and o1-style reasoning models — they emit chain-of-thought into the answer and break the RAG flow.
Start the local server (LM Studio → Developer tab → Start Server). Default address:
http://localhost:1234.Open the Memory Vault dashboard → Chat page → ⚙️ Settings → confirm the Local LLM URL points at your LM Studio instance. The model is auto-detected.
That's it. Ask a question; the dashboard retrieves relevant chunks, sends them with your question to LM Studio, and streams the answer back.
How it works
Hybrid search retrieves the top chunks for your question (same engine as
/api/searchand MCPrecall)Top hits are packed into a 6,000-token context budget — oldest history dropped first, then lowest-similarity chunks
LM Studio generates an answer streamed token-by-token via Server-Sent Events
Sources arrive first in the stream, so the UI shows "based on N memories" before tokens start flowing
Why LM Studio first
LM Studio's native API supports reasoning="off", which is the only reliable way to suppress chain-of-thought from thinking models in a RAG flow. Memory Vault uses the native API by default and falls back to OpenAI-compat (/v1/chat/completions) with <think>...</think> stripping if the native API isn't available.
REST API
Every MCP tool is also exposed as an HTTP endpoint so you can integrate Memory Vault into any app, script, or language. The API is served by FastAPI at http://localhost:8000 when you run docker compose up.
Interactive docs: http://localhost:8000/docs
OpenAPI schema: http://localhost:8000/openapi.json
The auto-generated /docs page is the canonical API reference — it stays in sync with the code. The summary below is for orientation.
Authentication
All endpoints except /api/health require a bearer token. Create one via the CLI:
docker compose exec app memory-vault token create my-appThe plaintext token is shown once — copy it immediately. Then send it as a header:
curl -H "Authorization: Bearer mv_..." http://localhost:8000/api/spacesManage tokens:
memory-vault token list
memory-vault token revoke mv_abc1234To disable auth entirely (local dev only), set API_AUTH_ENABLED=false.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/health | Service + database health (no auth) |
GET | /api/spaces | List memory spaces with chunk counts |
POST | /api/search | Hybrid search (vector + full-text + RRF) |
GET | /api/search/quality | How well recent searches have been matching |
GET | /api/chunks | List chunks with pagination and filters |
GET | /api/chunks/{id} | Get a single chunk |
DELETE | /api/chunks/{id} | Soft-delete (forget) a chunk |
POST | /api/ingest/text | Ingest a text string as a chunk |
POST | /api/ingest/file | Upload a file through the ingestion pipeline |
POST | /api/chat | RAG chat over hybrid search (non-streaming) |
POST | /api/chat/stream | RAG chat with token-by-token SSE streaming |
Example — search
curl -X POST http://localhost:8000/api/search \
-H "Authorization: Bearer $MV_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "how does hybrid search work",
"spaces": ["default"],
"limit": 5
}'Example — search quality
curl http://localhost:8000/api/search/quality \
-H "Authorization: Bearer $MV_TOKEN"{
"queries": 42,
"window_hours": 24,
"avg_top_similarity": 0.51,
"weak_matches": 6,
"empty_results": 0,
"weak_threshold": 0.35
}Aggregated from searches that already ran — asking costs nothing and runs no
query of its own. It reports each search's best hit rather than the mean of
its top-K, because a search returns as many rows as you asked for whatever the
vault holds; averaging down the ranks measures your limit as much as the
quality of the match.
weak_matches is the number worth watching. A search that finds nothing
relevant still returns its closest guesses, so empty_results stays near zero
even when nothing is being answered well. A high weak count usually means the
answers were never stored, rather than that search is failing to find them.
The Stats page in the web UI shows the same figures.
Example — ingest text
curl -X POST http://localhost:8000/api/ingest/text \
-H "Authorization: Bearer $MV_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "Decided to use RRF for hybrid merging", "space": "default"}'Example — upload a file
curl -X POST http://localhost:8000/api/ingest/file \
-H "Authorization: Bearer $MV_TOKEN" \
-F "[email protected]" \
-F "space=default"Configuration
| Variable | Default | Description |
|---|---|---|
API_HOST | 0.0.0.0 | Bind address |
API_PORT | 8000 | Port |
API_AUTH_ENABLED | true | Set false to disable bearer auth (local dev only) |
API_CORS_ORIGINS | * | Comma-separated allowed origins, or * |
API_RATE_LIMIT_PER_MIN | 120 | Per-IP request limit per minute |
Dashboard
Memory Vault ships with a web UI baked into the same Docker image as the API — no separate deploy, no extra port. Open http://localhost:8000 in your browser after docker compose up.
Six pages:
Chat — talk to your vault with a local LLM, sources shown for every answer (default landing page)
Search — hybrid search with space filter, similarity scores, expandable hit content
Browse — paginated chunk list with space + sort filters, two-step inline delete
Graph — force-directed knowledge graph (Cytoscape.js), pan/zoom, click a node to see its mentions and related entities, filters for space / type / min-mentions / max-nodes
Ingest — paste text or upload files (one at a time in v1.0), per-file status, batch summary
Stats — system health, total chunks, spaces table with visual distribution, auto-refresh every 30s
Access
The dashboard uses the same bearer token as the API. Create one and paste it into the dashboard's token screen:
docker compose exec app memory-vault token create dashboardThe plaintext token is shown once — copy it immediately. Open http://localhost:8000, paste into the prompt, and the dashboard stores it in localStorage under memory-vault-token. You won't be asked again on that browser.
Rotating or revoking
# See which tokens exist
docker compose exec app memory-vault token list
# Revoke by prefix (shown in list output)
docker compose exec app memory-vault token revoke mv_abc1234
# Create a new one
docker compose exec app memory-vault token create dashboardAfter revoking, the dashboard will hit a 401 on its next request and auto-clear the stored token, forcing you to paste the new one.
Troubleshooting
Prompted for token every reload: your browser is blocking
localStorage(private mode, strict cookie settings). Use a normal window or allow storage forlocalhost.401 on every request: the token was revoked or
API_AUTH_ENABLEDchanged. Create a fresh token and paste it in.Dashboard shows but API calls fail with CORS: you're hitting the API on a different origin than the dashboard. The baked-in build avoids this — use
http://localhost:8000, not the dev server, unless you know what you're doing.Running the dev server:
cd web && npm install && npm run devserves the UI athttp://localhost:5173with API calls proxied to:8000. For development only.Windows-specific issues: see docs/windows.md.
Reporting a bug: run
docker compose exec app memory-vault diagnose(ormemory-vault diagnoseon the host for a fuller bundle includingdocker compose ps+ db logs). The command writes amemory-vault-diagnostic-YYYY-MM-DD-HHMMSS.zipcontaining app logs, status, OS info, and redacted env vars. Bearer tokens, passwords, andmv_tokens are auto-scrubbed — but please review the bundle before attaching it to a public GitHub issue.Quoting a request ID: every API response carries an
X-Request-IDheader (UUID hex). Include it in bug reports — it lets the maintainer grep the same request across the structured JSON logs.
How It Works
Hybrid Search
Memory Vault combines two search methods and merges the results:
Vector search — converts your query to an embedding, finds semantically similar chunks via HNSW index
Full-text search — keyword matching via PostgreSQL tsvector + GIN index
RRF merging — Reciprocal Rank Fusion combines both ranked lists so neither method dominates
This means you find the right memory whether you remember the exact words or just the concept.
Query Enrichment
Before searching, Memory Vault generates up to 3 query variations using the embedding model's WordPiece tokenizer to extract key technical terms. This improves recall without losing precision.
Ingestion Pipeline
Async queue-based pipeline with adapters for different input formats:
Markdown — splits by headings, preserves structure
Plain text — paragraph-based with smart merging
Claude JSON — parses Claude conversation exports
Knowledge Graph
Memory Vault extracts entities and relationships from every ingested chunk and stores them alongside your memories. Click the Graph page in the dashboard to see how the things you've stored connect to each other.
How extraction works:
spaCy NER — the small
en_core_web_smmodel (~15 MB, CPU-only) tagsPERSON,ORG, andPRODUCTentities, mapped to Person, Project, and Tool respectively.Concept extraction — multi-token noun phrases that appear at least twice within a chunk become Concept entities. No LLM, no API calls.
Co-occurrence relationships — any two entities found in the same chunk produce a
related_torelationship. Edge weight grows with co-occurrence count across chunks.Per-space deduplication — entities are deduplicated by
(lower(name), type, space), so the same entity stays one node within a space without merging across unrelated projects.
The whole pipeline runs synchronously on ingest, on the same CPU that runs the embeddings — no extra services, no external API costs.
These trade-offs are deliberate. spaCy + co-occurrence is fast, free, and gets you 80% of the way to a useful graph at 0% of the LLM cost. The honest gaps are documented in the Limitations section below.
Performance Tuning
Memory Vault ships with maintenance_work_mem = 1 GB as the default in the bundled docker-compose.yml. The stock PostgreSQL default is 64 MB, which makes HNSW index builds on pgvector painfully slow once your corpus grows past a few thousand chunks.
If you're running on a host with 16 GB of RAM or more, bumping this to 2 GB gives noticeably faster index rebuilds with no downside. Edit the command: block in docker-compose.yml:
db:
image: pgvector/pgvector:pg16
command:
- postgres
- -c
- maintenance_work_mem=2GBIf you're running on a small box (4 GB total RAM or less, e.g. a tiny VPS), you may want to drop this back down to 256 MB so the rest of the system has breathing room. Memory Vault still works at the stock 64 MB default — it's just slower on large index rebuilds.
Limitations
v1.0 limitations (honest)
English-only NER.
en_core_web_smis English-trained; non-English text gets little to no useful entity extraction. Hybrid search and chat work fine in any language — only the auto-extracted knowledge graph is English-limited.NER is context-dependent. spaCy decides PERSON vs. ORG based on the surrounding sentence. The same name can land as both Person and Project entities depending on syntactic role.
No fuzzy entity matching. "PostgreSQL" and "Postgres" are separate entities. No alias merging in v1.0.
No re-extraction on edit. If you forget a chunk and re-ingest a corrected version, the new entities are added; the old ones aren't cleaned up automatically.
Single-instance. No multi-user / multi-tenant. One vault per deployment. Team features are PRO.
LM Studio only for chat. No Ollama provider in v1.0.
PRO tier (planned)
Team features, advanced analytics, hosted tier. The free / open-source core stays free forever — open-core, not bait-and-switch.
FAQ
How is this different from claude-mem / cognee / Mem0?
Different layer of the stack. Filesystem-based tools (claude-mem, claudesidian, obsidian-second-brain) keep markdown notes on disk and use grep/read at retrieval time. They work great until your vault grows past a few thousand notes — then grep gets slow and semantic recall isn't there. Memory Vault is a database-backed memory layer (Postgres + pgvector + tsvector + RRF) designed to scale and to be built on top of. Frontend-agnostic. Use it through MCP, REST, the dashboard, or your own app — all equal first-class clients.
cognee and Mem0 are closer in stack but cloud-first or SDK-first. Memory Vault is self-hosted infrastructure-first.
Do I need a GPU?
No. Default embeddings (all-MiniLM-L6-v2, 384-d) and entity extraction (en_core_web_sm) both run on CPU. Local LLM chat uses LM Studio on whatever hardware you have — a modern 16 GB-RAM machine handles 7B-parameter models comfortably.
Is my data sent to the cloud?
No. Memory Vault is self-hosted end-to-end. Embeddings are local (sentence-transformers), entity extraction is local (spaCy), chat uses your local LM Studio instance. No telemetry, no API calls to OpenAI / Anthropic / anyone. Your data stays on your machine, period.
Can I use it without Claude?
Yes. The MCP integration is one of three interfaces. The REST API and dashboard work standalone. Use the chat page with any local LLM (LM Studio supports many open-weights models), or build your own AI tool on top of the API.
What languages does NER support?
English only in v1.0. The default spaCy model is English-trained — non-English text gets little to no useful entity extraction. No multilingual NER in v1.0. Hybrid search and chat work fine in any language; only the auto-extracted knowledge graph is English-limited.
How much disk and RAM does it need?
Roughly: 2 GB disk (Docker image + Postgres data + spaCy + embedding model), 1-2 GB RAM idle, 4 GB+ recommended for active use. The bundled config sets maintenance_work_mem=1GB for fast HNSW index builds — drop it to 256 MB on a small VPS.
Can I run multiple Memory Vault instances?
Yes. Each instance is a single docker compose up. Use separate compose project names (docker compose -p mv-personal up, docker compose -p mv-work up) to keep them isolated, or clone into separate directories.
What happens to my data on future updates?
Migrations are versioned and forward-only. docker compose pull && docker compose up -d runs new migrations on start. Schema changes will be additive within v1.x — no destructive migrations on a minor version bump. That's part of the v1.x semver promise.
Contributing
Memory Vault is MIT-licensed and PRs are welcome. See:
CONTRIBUTING.md — how to report bugs, suggest features, set up dev environment, coding conventions
CODE_OF_CONDUCT.md — Contributor Covenant v2.1
SECURITY.md — reporting vulnerabilities (email
[e-mail address on the upstream page](https://github.com/MihaiBuilds/memory-vault/blob/19d10c58319c63244f986ffa5bc6705f90e15d83/README.md), don't open public issues)docs/threat-model.md — what Memory Vault defends against, what it does not, and how to harden a deployment that goes beyond a single machine
Bug report template — includes
memory-vault diagnoseinstructionsFeature request template — frame the problem before suggesting a solution
Single-maintainer project. PRs are reviewed when I have time. Big features should be discussed in an issue first.
License
The core is MIT licensed — free forever. Everything that makes Memory Vault useful as a personal memory system (hybrid search, MCP integration, knowledge graph, dashboard, local LLM chat, Docker setup) will always be free and open source.
A PRO tier for teams and advanced features is planned.
Credits
@rivestack — Postgres + pgvector tuning tips that landed in v1.0 (
maintenance_work_mem=1GBfor fast HNSW builds). More of his suggestions are on the list.Beta testers — the people who cloned, broke, and reported things during M1-M7
The open source giants this is built on — PostgreSQL, pgvector, sentence-transformers, spaCy, FastAPI, FastMCP, React, Cytoscape.js
Acknowledgments
The GitHub contributor list credits everyone whose code is in the repo. This section acknowledges reporters and diagnosticians whose insight shaped shipped work but doesn't appear in commit authorship.
Leonard Janke (lcjanke2020), working with GPT-5.6-Sol through OpenAI Codex (@lcj-codex-coder) — 20+ issues reported across the v1.0.7–v1.0.10 window, spanning version-drift, MCP correctness, timezone handling, embedding-dimension validation, and knowledge-graph soft-delete semantics. Peer-tier diagnostics; every report came with a disposable-database reproduction.
@git-pharos — diagnostic bundle in #74 that exposed three underlying issues fixed in v1.0.7.
Rivestack — the per-query
ef_searchknob and warming the vector index at start-up, both shipped in v1.5.0. Operational pgvector experience: the kind of default that is fine in a demo and wrong at scale.
Security reports and correctness findings are always credited by handle, plus any format the reporter specified — including tooling attribution where requested.
Follow the Build
Website: mihaibuilds.com
Blog: mihaibuilds.com/blog
GitHub: @MihaiBuilds
X: @mihaibuilds
Watch the repo to follow along.
CHANGELOG.md
Changelog
All notable changes to Memory Vault are documented here.
The format follows Keep a Changelog 1.1.0, and this project adheres to Semantic Versioning.
Unreleased
[1.6.1] — 2026-10-09
Dashboard Chat works again with local models that have no reasoning toggle.
Fixed
Chat failed with a 400 on models without reasoning configuration. Chat calls LM Studio's native
/api/v1/chatfirst and sends"reasoning": "off"with it. Models that expose no reasoning configuration — Gemma among them — reject that parameter outright, and the fallback to the OpenAI-compatible/v1/chat/completionsonly ran on 404, 405 and 501, so the 400 surfaced asLLM errorand Chat was unusable with those models. Search, ingestion and MCP recall were unaffected. The fallback now also covers 400: any rejection of the native payload moves the request to the compatible endpoint, which sends noreasoningfield at all, so this covers other native-only parameters a future model may refuse rather than just this one. Each fallback is logged. Reported by @cosmicwombat (#245).Streaming chat errors now name the cause.
/api/chat/streamreported every failure asLLM error. Check server logs., so the status code and endpoint the response already carried never reached the user — diagnosing the bug above meant reading the LLM server's own logs. Streaming errors now include both.
[1.6.0] — 2026-09-10
The MCP server can now be reached over HTTP, so a client on another machine no
longer needs an SSH tunnel or docker exec to talk to a vault running in
Docker. Alongside it, two things that make a large vault easier to reason
about: a warning when a space has grown big enough to hurt recall, and a
diagnostic showing how well recent searches have actually been matching.
Nothing here requires action on upgrade. The HTTP transport is off unless you
turn it on, and stdio is untouched.
Added
Serve MCP over HTTP. Set
MCP_HTTP_ENABLED=trueand the MCP server mounts at/api/mcpon the same port as the REST API, speaking SSE at/api/mcp/sse. Clients that cannot spawn a local process — a harness on another machine, or anything pointed at a URL rather than a command — can now connect. Requested in #194.It is off by default, because on by default would publish a memory store on a network port. When enabled, every request needs a bearer token from
memory-vault token create: the same tokens the REST API uses, checked against the same table, so revoking one revokes it everywhere.The transport also answers only to hostnames it recognises —
localhostand127.0.0.1unless told otherwise — and returns 421 Misdirected Request to anything else. That is DNS-rebinding protection, and it is why reaching the server by container name or LAN address fails until you say so.Name the hosts the transport answers to.
MCP_HTTP_ALLOWED_HOSTSis a comma-separated list —MCP_HTTP_ALLOWED_HOSTS=vault.internal:8000. It replaces the default rather than adding to it, so includelocalhost:*if you still want local clients. Without this, any deployment reached by a name other than localhost is unusable.See how well recent searches have been matching.
GET /api/search/qualityreports the average best match across the last 24 hours, how many searches matched weakly, and how many returned nothing. The Stats page shows the same figures.It aggregates what searching already recorded, so asking costs nothing and runs no query of its own. It averages each search's best hit rather than the mean of its top-K: search returns as many rows as you ask for whatever the vault holds, so averaging down the ranks measures your
limitas much as the quality of the match.Watch
weak_matchesrather thanempty_results. A search that finds nothing relevant still returns its closest guesses, so a vault can answer badly without ever returning zero results.Say when a space has grown large enough to split. Past 5,000 memories in a single space, the Stats page says so. Nothing breaks at that point and search keeps working — what degrades is precision, as a query competes against everything in the space. Splitting by topic and searching a narrower space usually helps more than tuning the query.
Fixed
The MCP server introduced itself without a version. Clients that show a server list displayed Memory Vault with an empty version string — the SDK defaults it to empty and nothing was passing one. It now reports the installed version, read from the package so it cannot drift from the version bump a release already does.
Security
browserslistbumped past a prototype-write advisory (4.28.4 → 4.28.9). A build-time dependency only — it never reaches the published image or the production bundle — and the advisory needs an attacker-controlled stats file this project does not have. Updated regardless.
Changed
Dependency updates:
psycopg≥3.3.5,pydantic≥2.13.5,ruff≥0.16.6, and nine web packages includingreact-router-dom7.18.3 andeslint10.10.0.
[1.5.0] — 2026-09-05
Eight additions, most of them about doing something with a graph you have already built: reading the memories behind a node, filtering by more than one type at a time, and merging the entities that extraction split apart. Plus a Windows fix that had been waiting on a reproduction.
Nothing here requires action on upgrade. Every new parameter is optional, and every existing call behaves as it did.
Added
Merge entities the extractor kept apart. Extraction is literal and per-occurrence, so one person arrives as "Alice", "Alice Smith" and "A. Smith".
POST /api/graph/entities/mergefolds one into another in a single transaction. Deciding that two entities are the same is a judgement call, so it is offered rather than guessed at. Merging across spaces is refused: entities are per-space by design, and a cross-space merge would move data between spaces without saying so.Read the memories behind a graph node.
GET /api/chunks?entity_id=…returns the chunks that mention an entity, and the graph's node panel links into Browse with that filter applied. The node used to be a dead end — you could see that something mattered and how often it came up, but not read what it came from.Filter the graph by several types at once.
?type=Person,Toolon the entity, relationship and visualization endpoints. Selecting two types meant two requests and no way to see both together. A single value still behaves exactly as before.Delete a space.
DELETE /api/spaces/{name}removes one that holds nothing, with a two-step delete on the dashboard. Spaces could be created but never removed, so a typo was permanent. A space with contents is refused rather than emptied — and "empty" means no memories and no graph entities, since deleting a space cascades into its entities.Upload several files in one request.
POST /api/ingest/filesanswers per file: one malformed file in a batch of thirty does not discard the other twenty-nine, and the response says which one to fix. Bounded per file, in aggregate, and by count.Tune search breadth per query.
ef_searchon/api/searchand the MCPrecalltool widens the HNSW search for one query without changing the server default for everyone. Bounded 1–1000.The vector index is warmed at start-up.
memory-vault warm-indexruns after migrations, so the first real search is not the one that pays to read the index off disk. Warming never blocks start-up: a container that refuses to start because an optimisation failed is worse than a slow first query.
Fixed
Memory Vault now starts cleanly on Windows. Windows defaults to an event loop psycopg refuses to use, so every connection attempt failed instantly and the pool retried — a burst of identical warnings and a multi-second delay before anything worked. A compatible loop is now selected at import. Reproduced on Windows 11 beforehand and measured after: three warnings and a pool timeout became zero warnings. Other platforms are untouched. (#77)
Contributors
Rivestack suggested both the per-query
ef_searchknob and warming the vector index at start-up.
1.4.0 — 2026-08-23
Ten bug fixes and one new capability. Most of these are the kind that only surface once you have been using Memory Vault for a while: a budget that was not really a cap, a chunk limit that was not really a limit, forgotten memories that never actually went away.
Nothing here requires action on upgrade.
Added
Forgotten memories can be purged.
forgetis a soft delete so a memory can be recovered, but nothing ever removed the rows afterwards — and editing a memory means forget plus remember, so a vault that is edited often grew a dead row per edit.purge_forgotten(older_than_days=30)over MCP, ormemory-vault purge-forgottenon the command line, deletes them for good. It is deliberately not automatic: Memory Vault runs no timer, so nothing deletes your memories unless you ask. The default only removes what was forgotten at least a month ago, so purging right after an accidental forget still spares it. (#74)memory_statusreportsforgotten_chunks. The number was derivable by subtracting, but naming it is what tells you whether purging is worth doing. (#74)
Fixed
A single oversized result could blow the token budget. Both the chat and MCP budget helpers admitted one arbitrarily large result — 2540 tokens against a 200 budget in the reported case, and the caller was not even told it had happened. Keeping at least one result is deliberate, since an answer with no context is worse than one with trimmed context; sending it whole was not. The final result is now trimmed to what is left. (#99)
max_wordswas not an upper bound. A sentence longer than the limit had no sentence boundary to split on, so it passed through whole — and because the flush was guarded on the accumulator being non-empty, an oversized first sentence escaped even with more text after it. Word splitting is now the floor under everything else. (#103)Uploaded files recorded the server's temporary path as their source. Every chunk from an upload pointed at something like
/tmp/tmpabcd1234.md, deleted the moment the request ended. Uploads now record the filename you sent — which also repairs re-upload deduplication, since a path that changes every request could never match. (#101)Repeated entity occurrences collapsed to one graph mention. "Alice met Alice" recorded one mention rather than two. Entity identity is still deduplicated — repeated mentions resolve to one node — but each occurrence now keeps its own offsets. Expect mention counts to rise as content is re-ingested. (#110)
An empty environment variable crashed at start-up.
os.getenvfalls back to its default only when a key is absent, soDB_PORT=""reachedint()and raised. Config generated from a manifest emits every declared key, empty where it has no value, which made this a first-run failure. Empty now means unset, and a malformed value names the setting instead of failing from inside the standard library. (#181)Concurrent creation of the same space returned 500. Two callers could both see no existing row and both insert; the loser hit the unique constraint. The insert is now authoritative and the loser gets the same 409 as any other duplicate. (#112)
A malformed UUID in a path returned 500.
not-a-valid-identifierreached PostgreSQL and became a server error. Those routes now reject it at the boundary without opening a database connection. (#102)Adding a file after a finished batch re-uploaded the completed ones. The submit loop ran over every file regardless of status, so a file already ingested went again — while the button correctly offered to ingest only the new one. (#106)
Stopping a chat left the answer marked as streaming. The input unlocked and the request ended, but the turn kept showing "Thinking…" indefinitely. It now reaches a terminal state while keeping whatever text had arrived. (#107)
Contributors
@lcj-codex-coder (Leonard Janke — lcjanke2020, working with GPT-5.6-Sol through OpenAI Codex) — reported #99, #101, #102, #103, #106, #107, #110, #112
@git-pharos — reported #74, the unbounded growth that
purge_forgottenanswers
1.3.0 — 2026-08-23
Least privilege. The containers no longer run as root, the database role that serves requests can no longer change your schema, API tokens can expire, and the threat model that describes all of it is now written down.
Nothing here requires action on upgrade. The container hardening applies automatically; the database roles and token expiry are opt-in and existing deployments keep working unchanged.
Added
A published threat model.
docs/threat-model.mdsets out what Memory Vault protects, what it assumes about its environment, and what it explicitly does not defend against — stolen tokens carry full access, spaces are not a security boundary, content is stored unencrypted, and prompt injection is not filtered. It also covers hardening a deployment that goes beyond a single machine: token hygiene, network scoping, egress policy, TLS termination, and rate limiting. (#18)Optional expiry for API tokens.
memory-vault token create <name> --expires-in-days N. A token created without an expiry never lapses, so every token issued before this release keeps working.token listgains anEXPIREScolumn and marks lapsed tokens. An expired token is reported differently from a revoked one, because an operator debugging a 401 needs to know which.Least-privilege database roles. Migration 009 defines
memory_vault_app(read and write rows),memory_vault_readonly(select only), andmemory_vault_migrator(schema changes). They are group roles with no login of their own, so nothing changes until you adopt them —DB_MIGRATION_USERandDB_MIGRATION_PASSWORDlet migrations run as a role the serving connection does not use. Adoption steps are in the threat model.
Changed
The containers run as a non-root user and need no writable filesystem. Both images create a system user (uid 10001), and the shipped compose file adds a read-only root filesystem, drops all Linux capabilities, and sets
no-new-privileges. The embedding model is now baked into the image at build time, which makes that possible and also removes a network round trip from the first query after every start.
Fixed
A read-only container no longer refuses to start when it cannot write its log file. The existing guard covered creating the log directory, but the directory ships inside the image — so the failure landed on opening the file instead, and a deployment with a read-only root filesystem and no log volume crashed at boot. It now warns and continues logging to stderr.
The threat model's description of the authentication boundary was wrong.
/docs,/redocand/openapi.jsonare unauthenticated and exempt from rate limiting, which the document did not say. No memory content is exposed and every documented operation still requires a token, but on a publicly reachable host they describe the API to anonymous visitors. The document now lists them and suggests blocking them at a reverse proxy.
Security
Public artifacts are scanned in CI. Local git hooks only ever see a command line, so anything written through the GitHub API — a pull request body passed as a file, an issue edited in the browser, release notes — never reached one. A workflow now scans pull request and issue text, release notes, and added lines in a diff. It fails closed: a missing pattern file, an empty one, or a malformed pattern all fail the run.
Dependencies
ruff
>=0.16.3, setuptools>=84.0.0, uvicorn>=0.52.3, spacy>=3.8.15(#158, #160, #169)Eight web dependencies updated as a group, including cytoscape 3.34.1 and vite 8.2.2 (#170)
1.2.1 — 2026-08-18
Patch release. Two retrieval and observability fixes, both reported and fixed by an outside contributor running Memory Vault at scale.
Fixed
Importance and recency boosts no longer bury exact matches. The boosts were added to the RRF score, but the maximum boost (0.20) was roughly eighteen times the entire spread between a rank-1 and a rank-50 result (~0.011). Relevance was effectively the tie-breaker and importance the primary sort key, so a recent high-importance note could outrank an exact match on an unrelated query. The boosts now scale the retrieval score instead of being added to it: they still order results of comparable relevance, but can no longer invert a clear rank difference. (#163, #165)
MCP
recallnow records queries inquery_log. The REST search endpoint was the only caller of the query logger, so on an MCP-only deploymentmemory_statusandmemory://statsalways reportedqueries_24h: 0and a null average latency however much the vault was used. (#164)
Contributors
@ghawley365 (Gary Hawley) — reported and fixed #163 (via #165) and #164
1.2.0 — 2026-08-17
Correctness release. Nine bugs fixed, two features added, and the MCP SDK migrated to 2.x. Most of the bugs are concurrency or failure-path defects that only show up under load or once something else has already gone wrong: a memory stored twice, a retry that duplicated a file, a connection pool that never recovered, a process that aborted outright.
Four migrations ship here (004–007). They apply automatically on start. Existing rows are not rewritten and no backfill runs, so upgrading does not touch data already stored.
Fixed
Concurrent
remembercalls no longer store the same memory twice. The duplicate check and the insert were separate statements with nothing holding a lock between them, so two simultaneous calls could both pass the check and both commit. A unique index now backs the guarantee and the insert resolves the conflict itself. Migration 004 collapses duplicates left behind by the race before creating the index, so a database that already hit the bug still upgrades. (#111)A failed file ingestion no longer leaves partial chunks behind, and retrying no longer duplicates them. Each chunk was committed on its own, so a failure part-way through left the earlier chunks stored with nothing recording that the file was incomplete. A file's chunks now share one transaction, and migration 005 gives each chunk an identity so re-ingesting the same file is a no-op rather than a second copy. A document that legitimately repeats a passage still stores both copies. (#115)
Concurrent embedding calls no longer abort the process. The embedding model is a single shared object whose forward pass is not safe to run from several threads at once, so two simultaneous requests could kill the process rather than raise an error. Embedding is now serialised, and the lazy model load no longer races. (#148)
A connection pool that fails to open is no longer cached. If the database was not accepting connections at start-up, the failed pool was stored and handed to every later caller, so the process never recovered even once the database came back. (#117)
Database credentials containing URI-reserved characters now work. A password containing
/,@,:or#broke connection-string parsing. Connection parameters are now assembled by the driver instead of being interpolated into a URI. (#113)/api/healthreturns 503 when the database is unreachable. It returned 200 with adegradedbody, and the Docker healthcheck only inspects the status code — so a container whose database had gone away still reported itself healthy. The body keeps its shape for operators reading it directly. (#108)Embedding no longer blocks the event loop. Inference ran synchronously inside async request handlers, so one embedding call stalled every other request in flight. Four call sites now run it in a worker thread. (#116)
memory-vault ingestexits non-zero when any file fails. It printed the failure count and exited 0, so scripts and CI treated a failed ingestion as a success. The individual errors are now printed as well. (#104)Streamed answers are no longer discarded when a
</think>tag is split across chunks. Inside a thinking block the parser cleared its whole buffer whenever it did not find a complete closing tag, throwing away a trailing fragment that the next chunk would have completed — and the answer with it. The same fix covers a second case where a response ending in a<character was truncated. (#98)
Added
Spaces are created on first write. Ingesting into a space that did not exist yet returned 404, so a space had to be created in a separate request before anything could be stored in it.
POST /api/ingest/textandPOST /api/ingest/filenow create it. The name still has to be one the API would accept from an explicit create, so a typo fails rather than quietly producing a near-identical space. The MCPremembertool deliberately still refuses unknown spaces and lists the ones that exist. (#153)Memories can be moved between spaces.
move_memoryon the MCP surface andPOST /api/chunks/{id}/movechange a memory's space without re-embedding it, and rebuild its knowledge-graph entries in the target space so the graph and search agree about where it lives. The target space must already exist. (#154)
Changed
The MCP SDK is now 2.x (
mcp>=2.0.0,<3.0.0). 2.0.0 removedmcp.server.fastmcp, which is why the SDK had been pinned below it; the class moved tomcp.server.mcpserverand was renamed. No behaviour change — the server exposes the same tools over the same transport. (#141)Knowledge-graph queries read through database views. Excluding forgotten memories was restated by hand in every graph query, which worked but left the next one a missed clause away from surfacing them. Migration 007 states the rule once. No behaviour change. (#155)
Contributors
@lcj-codex-coder (Leonard Janke — lcjanke2020, working with GPT-5.6-Sol through OpenAI Codex) — reported #98, #104, #108, #111, #113, #115, #116, #117
@gatesl — #52 (the space-lifecycle design that
_ensure_spaceandmove_memoryare built on)
1.1.1 — 2026-08-16
Small patch release: three dependency bumps merged one-by-one with per-PR throwaway-stack smoke tests. No code authored by MV. No behavior changes to the runtime, no schema changes, no migrations. Safe upgrade.
Changed
vitebumped to^8.2.1(was^8.2.0) in the web dev-deps group. Docker build smoke passed (Node/Vite bundle stage + Python image stage). (#138)uvicorn[standard]requirement raised to>=0.52.2,<1(was>=0.51.0,<1). Resolved to 0.52.3 in-container. ASGI server smoke: throwaway stack boots cleanly,/api/healthresponds. (#140)sentence-transformersrequirement raised to>=5.7.0,<6(was>=5.6.1,<6). Semantic-search smoke: two-topic ranking test returns byte-identical similarity scores to v1.1.0 — no embedding drift, users don't need to re-embed. (#139)
1.1.0 — 2026-08-08
Housekeeping release: dependency upgrades + external contributor docs. No code authored by MV — every change is either a merged upstream dependency bump or a docs contribution. All 5 Python dependabot PRs merged one-by-one with a throwaway-container smoke test between each; TS 7 major deferred (upstream peer-dep blocker); web-deps group merged as-is.
Changed
python-multipartrequirement raised to>=0.0.32(was>=0.0.29). Ships upstream file-upload parser speedups. Smoke: throwaway-stack file upload via/api/ingest/filesucceeds end-to-end. (#94)psycopg-poolrequirement raised to>=3.3.1,<4(was>=3.1,<4). DB pool client bump. Smoke: 5 rapid-fire ingests + 5 searches exercise pool checkout/return cleanly, no errors in pool logs. (#96)ruffrequirement raised to>=0.16.1(was>=0.15.20). Pre-merge verification:ruff check src/ tests/+ruff format --checkboth clean on the current codebase with 0 new findings. (#93)fastapirequirement raised to>=0.141.1,<1(was>=0.136.1,<1). 5-minor jump; highest Python-side risk in the batch. Smoke: 12/12 endpoint sweep passed — health, spaces list+create, ingest text+file, search, chunks list+get, graph entities+visualize,/docs, 401 handling. (#95)sentence-transformersrequirement raised to>=5.6.1,<6(was>=5.5.0,<6). Upstream 5.6.1 patches a RoBERTa-family flash-attention quality bug (not relevant to MV — we useall-MiniLM-L6-v2). Semantic smoke: two-topic ranking test confirms embedding quality (database-topic query: 0.71 vs 0.06; food-topic query: 0.56 vs -0.06; correct ranking both directions). (#91)Web dev-deps refreshed as a group.
@types/node26.1.1→26.1.2,@types/react19.2.17→19.2.18,@types/react-dom19.2.3→19.2.4,@vitejs/plugin-react6.0.4→6.0.5,eslint10.7.0→10.8.0,globals17.7.0→17.9.0,typescript-eslint8.65.0→8.66.0,vite8.1.5→8.2.0. Smoke: full docker image build succeeds (both Node/Vite bundle stage and Python image stage).typescriptintentionally held at 6.x — see Deferred. (#134)
Documentation
README installation section restructured.
uv syncis now the recommended path with an explicitpip + venvfallback block for users who don't wantuv. The missingpython -m spacy download en_core_web_smstep is now documented in both paths (previously caused a cryptic startup failure on first no-Docker run). (#136, based on #47)MCP setup section expanded. Global-scope config for Claude Code (
~/.claude/.mcp.json+enabledMcpjsonServers) is now documented alongside project scope. MCPcommanduses.venv/bin/pythonso the config works without needing the venv pre-activated. DockerDB_HOSTguidance clarified (127.0.0.1vslocalhost). (#136, based on #47)MCP Troubleshooting subsection added. Covers
ModuleNotFoundError, spaCyOSError [E050],claude --debug mcp, missingenabledMcpjsonServers, and the DockerDB_HOSTrefused-connection case. (#136, based on #47)MCP Registry status badge added to README. (#135)
Deferred
TypeScript 6.0.3 → 7.0.2 blocked by upstream peer-dep cap.
[email protected]requirestypescript >=4.8.4 <6.1.0, so TS 7 is not installable alongside our current lint tooling. Dependabot will reopen the upgrade PR when[email protected]ships with TS 7 support. No runtime impact (TS is a build-time tool, and TS 6 vs 7 produces functionally-identical JS output for our code). (#92)
Contributors
@skorten (Sean Korten) — #47 (uv setup, global MCP scope, troubleshooting section — adapted and shipped in #136)
1.0.10 — 2026-08-08
Added
Automatic MCP Registry publishing on release tag. Every
v*.*.*tag now pushes the checked-inserver.jsontoregistry.modelcontextprotocol.iovia GitHub OIDC — no stored secret required. Prior releases left the registry listing to drift; v1.0.10 is the first release where the registry, the ghcr image, and the git tag stay in lockstep automatically. (#125)MCP server import smoke test.
tests/test_mcp_server_import.pyimportsmemory_vault.mcp.serverand asserts the FastMCP instance + the four canonical tools exist. Two lines of test that would have failed CI on v1.0.9 and blocked the bad release. (#127)
Changed
mcpdependency pinned to>=1.28.1,<2.0.0. mcp 2.0.0 (released 2026-07-28) removedmcp.server.fastmcp, which the MCP server module imports; the unpinned spec silently resolved into a broken install. Pin resolves to mcp 1.29.0, the final maintained 1.x release cut on the same day as 2.0.0 for exactly this scenario. Migration to the mcp 2.0.0 API shape is planned for v1.1. (#127)MCP
remembernow runs the same graph extraction as REST/file ingestion. Previously the MCP surface inserted a chunk directly and skipped_run_extraction, so MCP-stored memories were searchable viarecallbut silently absent from every/api/graph/*surface. Extraction errors are still swallowed internally so the chunk stays committed even if spaCy fails. (#128)MCP
rememberrejects empty text and text over 1,000,000 characters at the boundary. MatchesIngestTextRequest'smin_length=1, max_length=1_000_000on the REST surface. (#128)sincetimestamp semantics onPOST /api/searchand MCPrecall. Offset-aware inputs like2026-01-01T00:00:00-05:00are now converted to UTC via.astimezone(UTC)instead of relabelled with.replace(tzinfo=UTC). Naive inputs still assume UTC per the documented API contract. (#124)
Fixed
recall(spaces=["unknown_name"])no longer widens the search to every space.resolve_space_names()returns[]for unknown names, and everyhybrid_searchcaller previously collapsed[]back toNoneviaspace_ids or None, so the space filter silently dropped out.[]now propagates through_build_where_clause, which emits a hardfalsepredicate. (#122)EMBEDDING_DIMENSIONSconfig mismatch fails fast at startup instead of crashing on the first embedding INSERT/SELECT. The pool-init path now queriespg_attributefor the actualvector(N)dimension onchunks.embeddingand raises with a clear message on mismatch. Skips cleanly on a fresh install wherechunksdoesn't exist yet. (#123)Forgotten chunks no longer leak through
/api/graph/*endpoints.DELETE /api/chunks/{id}marks a chunk as forgotten; the four graph endpoints (list entities, entity detail, list relationships, visualize) now apply the same predicate that search already uses.mention_countreports only live mentions, entities with zero live mentions drop out of listings, and forgotten-chunk preview text never surfaces via entity detail. Relationships withchunk_id IS NULL(future manual/LLM tagging) are preserved. (#129)
Fixed (P0 emergency)
v1.0.9 MCP-only Docker image was dead on arrival.
ghcr.io/mihaibuilds/memory-vault-mcp:1.0.9crashed at startup withModuleNotFoundError: No module named 'mcp.server.fastmcp'for every fresh pull since 2026-08-02. Root cause was the unpinnedmcpspec drifting into 2.0.0. Fresh source installs hit the same crash. Workaround for anyone on v1.0.9: useghcr.io/mihaibuilds/memory-vault-mcp:1.0.8or pinmcp<2.0.0in your own env. Fixed in v1.0.10 via the mcp pin above. (#126, #127)
Contributors
@lcj-codex-coder (Leonard Janke — lcjanke2020, working with GPT-5.6-Sol through OpenAI Codex) — reported #100, #105, #109, #114
1.0.9 — 2026-08-02
Added
Pool checkout validation:
AsyncConnectionPoolruns a liveness check on checkout so connections that died while idle are discarded and replaced transparently instead of surfacing as query failures. Fixes the "server closed the connection unexpectedly" errors common on remote-Postgres deployments. (#118)
Changed
Every runtime and release surface now agrees with the released tag.
/api/health, FastAPI/openapi.json,docker-compose.yml,server.json, andmemory-vault diagnoseall report the real installed package version, resolved viaimportlib.metadata. The release workflow now blocks a tag whose descriptors drift. (#97, #120)
Contributors
@lcj-codex-coder (Leonard Janke — lcjanke2020, working with GPT-5.6-Sol through OpenAI Codex) — #97 (version drift report)
1.0.8 — 2026-07-23
Maintenance release; no changelog was published at tag time.
1.0.7 — 2026-07-19
Fixed
memory-vaultCLI is now pip-installable. Every subcommand (status,migrate,ingest,search,mcp,api,token,space,diagnose) failed withModuleNotFoundError: No module named 'src'immediately on invocation for pip-installed deployments across every prior 1.0.x release. Docker deployments accidentally kept working via aPYTHONPATH=/appworkaround. Refactored to a propermemory_vaultpackage, updated the entry point, bundled the SQL migrations with the wheel, and removed the Docker workaround so containers and pip installs share a single install path. (#76, #84)pyproject.tomlversion now tracks the git tag. The package reported version 0.4.0 regardless of which 1.0.x tag was installed because the release workflow never bumped the version file. A one-time correction to 1.0.7 plus a version-guard job inrelease.ymlprevent drift going forward. (#75, #78)Docker base image aligned to Python 3.13. spaCy has no cp314 wheel yet, so every
docker buildfailed atpip install. Base image downgraded frompython:3.14-slimtopython:3.13-slim, matching the CI pytest matrix.dependabot.ymlignore entry added to prevent automatic bumps back to the bleeding edge. (#79, #80)
Known issues
Windows
ProactorEventLoopstartup warnings on stdio MCP deployments with remote Postgres. Reproduces on Windows 11 + Python 3.12 + psycopg async pool. Needs a controlled repro before choosing the fix. (#77)
Contributors
@git-pharos — #74 (diagnostic bundle that exposed three of the four fixes above)
1.0.6 — 2026-05-18
Fixed
Corrected casing of the
io.modelcontextprotocol.server.nameOCI annotation onmemory-vault-mcpfrom lowercase (mihaibuilds) to the actual GitHub org login casing (MihaiBuilds). MCP Registry publish was blocked with a 403 until this was corrected.
1.0.5 — 2026-05-18
Added
LABEL io.modelcontextprotocol.server.name="io.github.mihaibuilds/memory-vault"onDockerfile.mcp. The official MCP Registry uses this OCI annotation to verify that the publisher of aserver.jsonactually controls the image. Prep for registry submission.
1.0.4 — 2026-05-18
Added
New
memory-vault-mcpDocker image — thin MCP-only image that ships the MCP stdio server and nothing else. Connects to an external Postgres+pgvector via env vars. Intended for directmcp.jsonconfigurations, MCP catalog registry submissions, and larger Compose setups with shared Postgres. Multi-arch (amd64 + arm64). The existing all-in-one image continues unchanged.
1.0.3 — 2026-05-17
Changed
Docker base images:
python:3.11-slim→python:3.14-slim,node:20-slim→node:26-slim. (Reverted topython:3.13-slimin v1.0.7 due to spaCy wheel gap.)CI runners aligned to the same Python 3.14 / Node 26 as the shipped image (previously tests ran on 3.11/20 while the image shipped on 3.14/26 — a tested-vs-shipped divergence closed).
Tailwind CSS v3 → v4 on the web dashboard: full migration via the official codemod, including PostCSS plugin rename and config-as-CSS via
@theme.GitHub Actions:
actions/checkout@v4 → v6,actions/cache@v4 → v5, plus 5 Dependabot-bumped CI actions.Documented 4 intentional empty-
exceptfallback sites (CodeQL "Empty except") inchat.py,adapters/base.py,diagnose.py,tests/test_chat_api.py. No behavior change — comments only.
1.0.2 — 2026-05-08
Security
Path traversal in SPA fallback (High,
py/path-injection). The unauthenticated SPA fallback route accepted user-controlled paths and composed them with the static directory, allowing requests likeGET /../../etc/passwdto escape. Fixed via a_safe_static_pathhelper usingos.path.commonpath+os.path.realpathplus pre-composition rejection of empty / null-byte / leading-slash / explicit-traversal inputs. Three independent layers of defense. (CodeQL alert 2 + 3; #19)Information exposure in chat stream (Medium,
py/stack-trace-exposure). The inner SSE error handler in/api/chat/streaminterpolated raw exception text into the response. Fixed: server-sidelogger.exception(...), generic client message. (CodeQL alert 1; #19)
Notes
Three CodeQL partial-SSRF findings on the
llm_urlfield inChatRequestwere dismissed as architectural — Memory Vault is single-tenant self-hosted with bearer-token auth, and thellm_urlfield is intentional operator configuration. Hardening guidance for non-default deployments tracked in #18 for v1.1.
1.0.1 — 2026-05-07
Fixed
docker-compose.ymlnow references the published image instead of building from source. First-run on a fresh clone is now ~30 seconds (image pull) instead of ~5 minutes (local build). The README's "one-command Docker" promise is now actually one command.
1.0.0 — 2026-05-07
Added
Memory Vault v1.0 — first stable release. A long-term memory layer for AI assistants and the apps you build on top of them.
Hybrid search — pgvector HNSW + tsvector GIN, merged with Reciprocal Rank Fusion.
MCP server —
recall,remember,forget,statusfor Claude Desktop / Claude Code.Knowledge graph — spaCy NER + co-occurrence, no LLM cost, Cytoscape visualization.
Local LLM chat — LM Studio with a sources panel showing retrieved chunks per answer.
REST API — FastAPI, bearer auth, OpenAPI at
/docs.Memory spaces — namespacing for different contexts (work, personal, projects).
One-command Docker — multi-arch image (linux/amd64 + linux/arm64).
163 tests passing in CI against a real Postgres + pgvector service container.
171 files · 161 not served here
- .dockerignore (not served here)
- .env.example (not served here)
- .gitattributes (not served here)
- .github/FUNDING.yml (not served here)
- .github/ISSUE_TEMPLATE/bug_report.yml (not served here)
- .github/ISSUE_TEMPLATE/config.yml (not served here)
- .github/ISSUE_TEMPLATE/feature_request.yml (not served here)
- .github/dependabot.yml (not served here)
- .github/leak-patterns.txt (not served here)
- .github/pull_request_template.md (not served here)
- .github/workflows/codeql.yml (not served here)
- .github/workflows/leak-gate.yml (not served here)
- .github/workflows/lint.yml (not served here)
- .github/workflows/release.yml (not served here)
- .github/workflows/test.yml (not served here)
- .gitignore (not served here)
- ARCHITECTURE.md
- ATTRIBUTION.md
- CHANGELOG.md
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- Dockerfile (not served here)
- Dockerfile.mcp (not served here)
- LICENSE
- README.md
- SECURITY.md
- docker-compose.yml (not served here)
- docs/images/architecture.mmd (not served here)
- docs/images/architecture.png (not served here)
- docs/images/chat-with-sources.png (not served here)
- docs/threat-model.md (not served here)
- docs/windows.md
- mcp.json.example (not served here)
- pyproject.toml (not served here)
- scripts/security-pentest.sh (not served here)
- scripts/start.sh (not served here)
- server.json
- src/memory_vault/__init__.py (not served here)
- src/memory_vault/adapters/__init__.py (not served here)
- src/memory_vault/adapters/base.py (not served here)
- src/memory_vault/adapters/claude.py (not served here)
- src/memory_vault/adapters/markdown.py (not served here)
- src/memory_vault/adapters/plaintext.py (not served here)
- src/memory_vault/api/__init__.py (not served here)
- src/memory_vault/api/app.py (not served here)
- src/memory_vault/api/deps.py (not served here)
- src/memory_vault/api/middleware.py (not served here)
- src/memory_vault/api/routers/__init__.py (not served here)
- src/memory_vault/api/routers/chat.py (not served here)
- src/memory_vault/api/routers/chunks.py (not served here)
- src/memory_vault/api/routers/graph.py (not served here)
- src/memory_vault/api/routers/health.py (not served here)
- src/memory_vault/api/routers/ingest.py (not served here)
- src/memory_vault/api/routers/search.py (not served here)
- src/memory_vault/api/routers/spaces.py (not served here)
- src/memory_vault/api/schemas.py (not served here)
- src/memory_vault/api/server.py (not served here)
- src/memory_vault/cli.py (not served here)
- src/memory_vault/config.py (not served here)
- src/memory_vault/diagnose.py (not served here)
- src/memory_vault/extraction/__init__.py (not served here)
- src/memory_vault/extraction/graph_writer.py (not served here)
- src/memory_vault/extraction/spacy_extractor.py (not served here)
- src/memory_vault/logging_config.py (not served here)
- src/memory_vault/mcp/__init__.py (not served here)
- src/memory_vault/mcp/__main__.py (not served here)
- src/memory_vault/mcp/auth.py (not served here)
- src/memory_vault/mcp/http_transport.py (not served here)
- src/memory_vault/mcp/server.py (not served here)
- src/memory_vault/migrations/001_initial_schema.sql (not served here)
- src/memory_vault/migrations/002_api_tokens.sql (not served here)
- src/memory_vault/migrations/003_knowledge_graph.sql (not served here)
- src/memory_vault/migrations/004_chunk_content_hash_dedup.sql (not served here)
- src/memory_vault/migrations/005_chunk_ingest_identity.sql (not served here)
- src/memory_vault/migrations/006_scope_remember_dedup_index.sql (not served here)
- src/memory_vault/migrations/007_live_graph_views.sql (not served here)
- src/memory_vault/migrations/008_token_expiry.sql (not served here)
- src/memory_vault/migrations/009_least_privilege_roles.sql (not served here)
- src/memory_vault/models/__init__.py (not served here)
- src/memory_vault/models/db.py (not served here)
- src/memory_vault/services/__init__.py (not served here)
- src/memory_vault/services/embedding.py (not served here)
- src/memory_vault/services/graph.py (not served here)
- src/memory_vault/services/ingestion.py (not served here)
- src/memory_vault/services/search.py (not served here)
- src/memory_vault/services/spaces.py (not served here)
- tests/__init__.py (not served here)
- tests/conftest.py (not served here)
- tests/test_adapters.py (not served here)
- tests/test_api.py (not served here)
- tests/test_api_routers.py (not served here)
- tests/test_async_embed_yields_event_loop.py (not served here)
- tests/test_bad_input_matrix.py (not served here)
- tests/test_bulk_ingest.py (not served here)
- tests/test_chat_api.py (not served here)
- tests/test_chat_think_tag_boundary.py (not served here)
- tests/test_chunk_word_cap.py (not served here)
- tests/test_chunks_by_entity.py (not served here)
- tests/test_cli_exit_code.py (not served here)
- tests/test_container_hardening.py (not served here)
- tests/test_db_conninfo_encoding.py (not served here)
- tests/test_db_pool.py (not served here)
- tests/test_diagnose_redaction.py (not served here)
- tests/test_ef_search.py (not served here)
- tests/test_embedding_dimension_guard.py (not served here)
- tests/test_embedding_thread_safety.py (not served here)
- tests/test_ensure_space.py (not served here)
- tests/test_entity_merge.py (not served here)
- tests/test_entity_occurrences.py (not served here)
- tests/test_env_setting_parsing.py (not served here)
- tests/test_extraction.py (not served here)
- tests/test_graph_api.py (not served here)
- tests/test_graph_excludes_forgotten.py (not served here)
- tests/test_graph_type_multiselect.py (not served here)
- tests/test_ingest_atomicity.py (not served here)
- tests/test_least_privilege_roles.py (not served here)
- tests/test_live_graph_views.py (not served here)
- tests/test_logging_readonly_fallback.py (not served here)
- tests/test_mcp_http_mount.py (not served here)
- tests/test_mcp_remember_concurrent_dedup.py (not served here)
- tests/test_mcp_remember_extraction.py (not served here)
- tests/test_mcp_server_import.py (not served here)
- tests/test_mcp_token_verifier.py (not served here)
- tests/test_migration_003.py (not served here)
- tests/test_move_memory.py (not served here)
- tests/test_purge_forgotten.py (not served here)
- tests/test_query_logging.py (not served here)
- tests/test_ranking_dominance.py (not served here)
- tests/test_search.py (not served here)
- tests/test_search_quality.py (not served here)
- tests/test_since_timestamp_parsing.py (not served here)
- tests/test_space_create_race.py (not served here)
- tests/test_space_delete.py (not served here)
- tests/test_space_filter_security.py (not served here)
- tests/test_token_budget_cap.py (not served here)
- tests/test_token_expiry.py (not served here)
- tests/test_token_lookup.py (not served here)
- tests/test_unicode_and_large_content.py (not served here)
- tests/test_upload_source_name.py (not served here)
- tests/test_uuid_path_validation.py (not served here)
- tests/test_version_reporting.py (not served here)
- tests/test_warm_index.py (not served here)
- tests/test_windows_event_loop.py (not served here)
- web/.gitignore (not served here)
- web/README.md (not served here)
- web/eslint.config.js (not served here)
- web/index.html (not served here)
- web/package-lock.json (not served here)
- web/package.json (not served here)
- web/postcss.config.js (not served here)
- web/public/favicon.svg (not served here)
- web/public/icons.svg (not served here)
- web/src/App.tsx (not served here)
- web/src/api.ts (not served here)
- web/src/auth.ts (not served here)
- web/src/components/ErrorBanner.tsx (not served here)
- web/src/components/NetworkBanner.tsx (not served here)
- web/src/components/TokenGate.tsx (not served here)
- web/src/cytoscape-cose-bilkent.d.ts (not served here)
- web/src/index.css (not served here)
- web/src/main.tsx (not served here)
- web/src/pages/Browse.tsx (not served here)
- web/src/pages/Chat.tsx (not served here)
- web/src/pages/Graph.tsx (not served here)
- web/src/pages/Ingest.tsx (not served here)
- web/src/pages/Search.tsx (not served here)
- web/src/pages/Stats.tsx (not served here)
- web/tsconfig.app.json (not served here)
- web/tsconfig.json (not served here)
- web/tsconfig.node.json (not served here)
- web/vite.config.ts (not served here)
Advantages
- No separate vector database is required as vector embeddings, full-text indexes, and relational data all live in Postgres. evidence: README.md
- The system is frontend-agnostic, allowing MCP, REST, CLI, and custom apps to act as equal first-class clients. evidence: README.md
Limitations
- Thinking models like Qwen3 or DeepSeek-R1 can break the RAG flow by emitting chain-of-thought into the answer. evidence: README.md
- The system does not include transport encryption for the MCP HTTP server, requiring external TLS configuration. evidence: README.md
Security
Risk level: No risk found
This brain runs
MCP servers (3)
README.md(memory-vault):/path/to/memory-vault/.venv/bin/python -m memory_vault.mcpREADME.md(memory-vault):/path/to/memory-vault/.venv/bin/python -m memory_vault.mcpREADME.md(memory-vault):http://192.168.1.50:8000/api/mcp/sse· uses the network
Attribution and licence
- Created by
- MihaiBuilds
- Declared authors
- MihaiBuilds (declared in pyproject.toml)
- Publisher
- Mihai Builds (Official Mihai Builds project)
- Source
- MihaiBuilds/memory-vault · commit 19d10c5
- Licence
- MIT License (files served here)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by MihaiBuilds or Mihai Builds.
Install from source
161 of its files are not served here, so there is no ZIP. MihaiBuilds/memory-vault at 19d10c5