A self-hosted memory layer that gives MCP-compatible AI clients durable, user-controlled context across sessions.
Personal Memory stores facts as semantic memory, retrieves the context relevant to the current conversation, and can optionally search a personal document library, expose Todoist tools, and visualize what has been remembered. The service runs on infrastructure you control and presents its capabilities through Streamable HTTP MCP endpoints.
Persistent facts are the core product. Document RAG, Todoist, OAuth onboarding, and the visualization dashboard are optional features that can be enabled independently.
- Why Personal Memory
- What It Can Do
- How It Works
- Quick Start
- Connect an MCP Client
- Using Personal Memory
- Core Concepts
- Tool Reference
- Developer and Operator Guide
- Configuration Reference
- Security
- Operations
AI conversations are usually stateless. A client may know your preferences, project decisions, or working conventions during one session and lose them in the next. Repeating that context in every prompt is slow, inconsistent, and difficult to maintain.
Personal Memory provides a durable context layer between you and your AI clients:
- save explicit facts, preferences, constraints, and decisions once;
- retrieve them by meaning rather than exact wording;
- organize them without coupling memory to a single client or model;
- keep temporary information from living forever;
- inspect, export, update, or delete what has been stored;
- optionally search your own Markdown and text files through the same MCP endpoint.
The result is continuity without embedding a growing personal profile into every prompt or tying it to one AI provider.
| Capability | What it provides | Availability |
|---|---|---|
| Semantic memory | Store, recall, update, export, and safely prune durable facts | Core |
| Document search | Hierarchical semantic search across Markdown and text files | Optional: ENABLE_RAG=true |
| Todoist tools | Read projects and labels; read and manage tasks without exposing the token to clients | Optional: ENABLE_TODOIST=true |
| Visualization | Browse facts by topic, timeline, similarity graph, duplicates, and document tree | Optional: ENABLE_VIZ=true |
| OAuth onboarding | Authenticate compatible clients with an external or self-hosted OIDC issuer | Optional: OAUTH_ENABLED=true |
| Snapshots | Create and retain Qdrant snapshots on a schedule | Built in |
Facts and documents are embedded by the TEI service inside the deployment. Optional Todoist integration calls the Todoist API, and OAuth mode depends on the issuer you configure.
flowchart LR
Client["MCP client"] -->|"HTTPS + API key or OAuth"| App["Personal Memory"]
App -->|"embed text"| TEI["Text Embeddings Inference"]
App -->|"facts and document vectors"| Qdrant["Qdrant"]
Docs["Markdown and text files"] --> Indexer["RAG indexer"]
Indexer --> TEI
Indexer --> Qdrant
App -. optional .-> Todoist["Todoist API"]
Browser["Browser"] -. optional .-> Viz["Visualization dashboard"]
Viz --> Qdrant
The Go server exposes two independent MCP endpoints:
/memorycontains semantic memory and, when enabled, document-search tools;/todoistexists only when Todoist integration is enabled.
TEI converts text into vectors. Qdrant stores those vectors and their metadata. The application performs retrieval, validation, deduplication, mutation safety checks, caching, backup scheduling, and graceful shutdown.
The repository Compose file starts three services: Personal Memory, TEI, and Qdrant. It is a deployment baseline rather than a standalone local sandbox. Before starting it, provide:
- Docker Engine and Docker Compose;
- a DNS record for
mcp.<your-domain>; - an existing external Docker network named
traefik; - Traefik v3 on that network with an
httpsentrypoint andletsEncryptcertificate resolver; - writable host directories for Qdrant data and snapshots.
Authentik ForwardAuth is required only when the visualization dashboard is enabled. The default memory-only deployment does not require Authentik.
git clone https://github.com/Dzarlax-AI/personal_memory.git
cd personal_memory
sudo mkdir -p /root/memory/qdrant_storage /root/memory/qdrant_snapshots
docker network inspect traefik >/dev/nullIf the final command fails, create or configure the external network before continuing. The included Compose file does not start Traefik itself.
cp .env.example .env
openssl rand -hex 32Put the generated secret in API_KEY, set MEMORY_DOMAIN, and review the optional feature flags in .env. Keep ALLOW_INSECURE_AUTH=false on every reachable deployment.
Minimal configuration:
MEMORY_DOMAIN=example.com
API_KEY=replace_with_the_generated_secret
ALLOW_INSECURE_AUTH=false
MEMORY_USER=your_name
ENABLE_RAG=false
ENABLE_TODOIST=false
ENABLE_VIZ=false
OAUTH_ENABLED=falsedocker compose up -d
docker compose ps
docker compose logs -f memory-embeddingsThe first TEI start downloads the pinned embedding model and may take longer than subsequent starts. Once the services are ready:
curl -fsS https://mcp.example.com/health
# ok
curl -fsS http://127.0.0.1:6333/healthzThe first endpoint verifies public routing to the Go service. The second verifies the host-local Qdrant port.
The checked-in Compose file does not mount a document directory. When enabling RAG, add a read-only bind mount to memory-mcp that matches RAG_DOCUMENTS_DIR:
services:
memory-mcp:
volumes:
- /srv/personal-documents:/root/documents/personal:roThen set:
ENABLE_RAG=true
RAG_DOCUMENTS_DIR=/root/documents/personal
RAG_REINDEX_INTERVAL_MINUTES=30Any filesystem synchronization mechanism can populate the host directory. The indexer only reads .md, .markdown, and .txt files.
Personal Memory uses the Streamable HTTP transport.
| Endpoint | URL | Authentication |
|---|---|---|
| Memory and RAG | https://mcp.example.com/memory |
X-API-Key or Bearer API key; OAuth bearer tokens when enabled |
| Todoist | https://mcp.example.com/todoist |
API key only; route absent when disabled |
Use one of these headers with the memory endpoint:
X-API-Key: <API_KEY>
or:
Authorization: Bearer <API_KEY>
Client command syntax can change; verify it against the version of your client. A current Streamable HTTP configuration looks like:
export MEMORY_API_KEY='<your API key>'
claude mcp add --transport http personal-memory \
https://mcp.example.com/memory \
--header "X-API-Key: $MEMORY_API_KEY" \
--scope userAdd Todoist as a separate server only after ENABLE_TODOIST=true:
claude mcp add --transport http personal-todoist \
https://mcp.example.com/todoist \
--header "X-API-Key: $MEMORY_API_KEY" \
--scope userUse an HTTP-to-stdio bridge such as mcp-remote. A ready-to-edit example is provided in claude_desktop_config.example.json.
When OAUTH_ENABLED=true, use the memory endpoint as the MCP server URL. Protected-resource metadata is available at:
https://mcp.example.com/.well-known/oauth-protected-resource
OAuth applies to /memory; the optional Todoist endpoint remains API-key-only.
Normal usage happens through an MCP client. The exact phrasing is up to the client, but the following prompts demonstrate the intended workflows.
Remember that I prefer PostgreSQL migrations to be reversible. Store it in the
technamespace with the tagspostgres,migrations, andpreference.
The client should call store_fact. Personal Memory embeds the fact, checks for near-duplicates, warns about related facts that may contradict it, and stores the metadata.
Before changing the database layer, recall my relevant PostgreSQL and migration preferences.
The client should call recall_facts with a natural-language query. Retrieval is semantic, so the stored wording does not need to match the query.
Remember that the analytics service uses ClickHouse for event storage. Put it in the
projectsnamespace, tag itanalytics,clickhouse, andarchitecture, withanalyticsas the primary tag.
Namespaces are broad context boundaries. Tags improve filtering and discovery. primary_tag selects the main grouping used by the visualization dashboard.
Search my documents for the reasoning behind the authentication architecture.
With RAG enabled, the client calls search_documents. Hierarchical mode first identifies relevant folders and then searches chunks within them; flat mode searches all chunks directly.
Show me facts older than 180 days that would be removed, but do not delete anything.
forget_old defaults to dry_run=true and never removes facts marked permanent=true. Use exact point_id values for deterministic updates or deletions when multiple semantic matches are close.
get_operational_context returns permanent facts plus the most frequently recalled non-permanent facts. Automation that needs plain text can call the authenticated endpoint:
GET /memory/operational?namespace=projects
A fact is a short piece of durable context with an embedding and metadata. Good facts are explicit preferences, decisions, constraints, identity details, or non-obvious project context. Long-form source material belongs in the document index instead.
Namespaces are broad isolation and filtering boundaries such as personal, projects, work, or tech. The default namespace is default, but clients should choose meaningful stable namespaces for long-lived data.
Point IDs include both namespace and exact text, so identical text can exist safely in different namespaces.
Tags are semantic labels. primary_tag is an optional single grouping label and must also be present in tags. When a fact has exactly one tag and no primary tag, that tag is promoted automatically.
The MCP schema accepts tags as one comma-separated string, for example tags="postgres,migrations,preference".
permanent=truepreventsforget_oldfrom deleting a fact.valid_until=YYYY-MM-DDexcludes an expired fact from semantic recall.recall_countandlast_recalled_atrecord how often stored context is used.
New facts at or above the deduplication threshold are not stored twice. Related facts below that threshold may be returned as contradiction warnings.
Semantic updates and deletions require a sufficiently strong, unambiguous match. The default mutation threshold is 0.90; candidates within 0.01 of each other are treated as ambiguous. Supplying a validated point_id bypasses similarity selection while still enforcing namespace checks.
The RAG index is separate from the fact collection. Documents are split along Markdown structure, embedded in batches, and stored as versioned chunks. For a changed file, the previous complete generation remains searchable until every new chunk has been embedded and written successfully.
| Tool | Purpose |
|---|---|
store_fact(fact, tags?, primary_tag?, namespace?, permanent?, valid_until?) |
Store a fact after deduplication and contradiction checks. |
update_fact(new_fact, old_query?, point_id?, tags?, primary_tag?, namespace?, permanent?) |
Replace a fact selected by a safe semantic match or exact ID. |
delete_fact(query?, point_id?, namespace?) |
Delete a fact selected by a safe semantic match or exact ID. |
forget_old(days=90, namespace?, dry_run=true) |
Preview or remove old non-permanent facts. |
import_facts(facts) |
Import a JSON-array string with at most 1,000 entries and 4 MiB. |
| Tool | Purpose |
|---|---|
recall_facts(query, namespace?, limit=5) |
Return semantically relevant, non-expired facts and increment recall counts. |
find_related(query, namespace?, limit=5) |
Return related facts below the duplicate threshold. |
list_facts(namespace?) |
List facts and metadata. |
get_stats() |
Summarize namespaces, tags, counts, and most-recalled facts. |
list_tags(namespace?) |
List tags and their usage counts. |
export_facts(namespace?) |
Export facts as JSON. |
get_operational_context(namespace?, top_recalled=10) |
Return permanent and frequently recalled context. |
These tools are registered on /memory only when ENABLE_RAG=true.
| Tool | Purpose |
|---|---|
search_documents(query, limit=5, mode="hierarchical") |
Search indexed chunks using hierarchical or flat mode. |
reindex_documents() |
Start one incremental background reindex; concurrent runs are rejected. |
These tools and the /todoist route exist only when ENABLE_TODOIST=true.
| Tool | Purpose |
|---|---|
get_projects() |
List Todoist projects. |
get_labels() |
List personal labels. |
get_tasks(project_id?, filter?, limit=20) |
List active tasks, optionally using Todoist filter syntax. |
create_task(content, project_id?, due_string?, priority?, labels?) |
Create a task. Priority is an integer from 1 to 4. |
update_task(task_id, content?, due_string?, priority?, labels?) |
Update at least one task field. |
complete_task(task_id) |
Complete a task. |
delete_task(task_id) |
Permanently delete a task. |
| Input | Limit |
|---|---|
| MCP request body | 4 MiB |
| Fact or replacement text | 64 KiB |
| Search or mutation query | 16 KiB |
| Namespace | 255 bytes |
| Tags | 100 tags, 255 bytes each |
Search limits and top_recalled |
Integer from 1 to 100 |
| Todoist labels | 100 labels, 255 characters each |
The included Compose file defines three services on an external traefik network:
| Service | Responsibility | Exposure |
|---|---|---|
memory-mcp |
Go HTTP server, MCP tools, visualization, backup scheduler | Routed by Traefik |
memory-embeddings |
Pinned TEI image and embedding model revision | Docker network only |
memory-qdrant |
Vector storage for facts and RAG collections | Docker network plus 127.0.0.1:6333 |
The Go process serves /memory, optional /todoist, optional /viz, OAuth metadata, and public /health on one port. Qdrant and TEI do not need public internet exposure.
Requirements:
- Go 1.24 or newer;
- Docker for image and Compose checks;
curlplussha256sumorshasumfor pinned browser assets.
Canonical verification:
make testThis downloads and checksum-verifies pinned visualization assets, runs go vet ./..., runs all Go tests, and builds all three commands.
Individual commands:
make dev-deps
go test ./...
go test -race ./...
go build ./cmd/server ./cmd/indexer ./cmd/migrate-memory-ids
docker build -t personal-memory .The final image contains:
/personal-memory— server entrypoint;/personal-memory-indexer— one-shot RAG indexer;/personal-memory-migrate-ids— memory ID migration utility.
cmd/
server/ HTTP and MCP server entrypoint
indexer/ standalone RAG indexer
migrate-memory-ids/ namespace-aware ID migration
internal/
backup/ Qdrant snapshot loop
config/ environment loading and validation
embeddings/ bounded TEI HTTP client
memory/ memory tools, cache, IDs, recall counter
memorymigration/ dry-run/apply migration logic
middleware/ authentication and request limits
oauth/ OIDC discovery and JWT verification
qdrant/ bounded Qdrant REST client
rag/ chunking, indexing, and document tools
todoist/ Todoist REST client and MCP tools
viz/ embedded visualization dashboard
Configuration is read from environment variables and validated at startup. Invalid booleans, numbers, URLs, thresholds, or conditional feature settings fail fast.
| Variable | Default | Description |
|---|---|---|
MCP_PORT |
8000 |
Internal HTTP port. |
MEMORY_DOMAIN |
— | Domain suffix used by Compose routing and as an OAuth resource default. |
API_KEY |
— | Shared MCP secret. Required unless OAuth is enabled or insecure development mode is explicit. |
ALLOW_INSECURE_AUTH |
false |
Allows startup and routes without configured credentials for isolated development. Configured API-key/OAuth checks remain active. Never enable on a reachable deployment. |
MEMORY_USER |
claude |
User label stored in fact metadata. |
QDRANT_URL |
http://memory-qdrant:6333 |
Qdrant base URL. |
EMBED_URL |
http://memory-embeddings:80 |
TEI base URL. |
| Variable | Default | Description |
|---|---|---|
CACHE_TTL |
60 |
Recall cache TTL in seconds. |
DEDUP_THRESHOLD |
0.97 |
Similarity at which a new fact is considered a duplicate. |
CONTRADICTION_LOW |
0.60 |
Lower bound for related-fact contradiction warnings. |
MUTATION_MATCH_THRESHOLD |
0.90 |
Minimum score for semantic update/delete selection. |
| Variable | Default | Description |
|---|---|---|
OAUTH_ENABLED |
false |
Allow OAuth bearer tokens on /memory. |
OAUTH_ISSUER |
— | Required HTTP(S) issuer URL when OAuth is enabled. |
OAUTH_RESOURCE |
https://mcp.<MEMORY_DOMAIN> |
Canonical MCP resource URL. |
OAUTH_AUDIENCE |
OAUTH_RESOURCE |
Required JWT audience. |
OAUTH_SCOPES |
memory:mcp |
Comma-separated required scopes. |
OAUTH_JWKS_URL |
discovered | Optional explicit JWKS URL. |
OAUTH_AUTHORIZATION_SERVERS |
OAUTH_ISSUER |
Comma-separated authorization-server URLs. |
OAUTH_RESOURCE_DOCUMENTATION |
— | Optional documentation URL in protected-resource metadata. |
| Variable | Default | Description |
|---|---|---|
ENABLE_TODOIST |
false |
Register the Todoist client, tools, and route. |
TODOIST_TOKEN |
— | Required only when Todoist is enabled. API_KEY is also required because the endpoint is API-key-only. |
ENABLE_VIZ |
false |
Mount the visualization dashboard. |
VIZ_PROXY_SECRET |
— | Required with visualization unless insecure development mode is explicit. Shared only by Traefik and the application. |
VIZ_SIMILARITY_THRESHOLD |
0.65 |
Default graph-edge similarity threshold. |
| Variable | Default | Description |
|---|---|---|
ENABLE_RAG |
false |
Register document tools and indexing lifecycle. |
RAG_DOCUMENTS_DIR |
/root/documents/personal |
Root directory for .md, .markdown, and .txt files. |
RAG_CHUNK_MAX_BYTES |
1500 |
Chunk target, validated from 1 byte to 1 MiB. |
RAG_FOLDER_TOP_K |
3 |
Number of folders considered by hierarchical retrieval. |
RAG_FOLDER_THRESHOLD |
0.50 |
Minimum folder score before falling back to flat search. |
RAG_COLLECTION_CHUNKS |
doc_chunks |
Qdrant chunk collection. |
RAG_COLLECTION_FOLDERS |
doc_folders |
Qdrant folder-summary collection. |
RAG_REINDEX_INTERVAL_MINUTES |
0 |
Automatic reindex interval; zero disables scheduling. |
| Variable | Default | Description |
|---|---|---|
BACKUP_INTERVAL_HOURS |
24 |
Snapshot interval. Must be positive. |
KEEP_SNAPSHOTS |
7 |
Number of snapshots retained. Must be at least one. |
EMBED_MODEL |
intfloat/multilingual-e5-small |
TEI model selected by Compose. |
EMBED_MODEL_REVISION |
pinned commit | Immutable model revision selected by Compose. |
Startup fails when neither API-key nor OAuth authentication is configured. ALLOW_INSECURE_AUTH=true is an explicit development escape hatch, not a production setting. Secret comparison uses constant-time checks.
/health is intentionally public. Memory and its operational endpoint are authenticated. Todoist is registered only when enabled. Request bodies and tool inputs are bounded before expensive embedding or database work.
The dashboard is intended to sit behind Authentik ForwardAuth. After authentication, Traefik overwrites X-Personal-Memory-Proxy-Secret; the application verifies the independently generated VIZ_PROXY_SECRET. Do not expose the container port directly when visualization is enabled.
- keep
.envout of version control; - use independent values for
API_KEYandVIZ_PROXY_SECRET; - do not send
TODOIST_TOKENto MCP clients; - expose Qdrant and TEI only on trusted networks;
- use immutable application image tags or digests for production deployments.
Trigger an incremental reindex through MCP with reindex_documents, schedule it with RAG_REINDEX_INTERVAL_MINUTES, or run the standalone binary:
docker compose exec memory-mcp /personal-memory-indexerThe indexer skips unchanged files by SHA-256 hash and detects incomplete generations. Hidden directories are skipped. Stale cleanup is aborted when the filesystem walk is incomplete or would unexpectedly remove more than half the indexed files.
The server creates Qdrant snapshots every BACKUP_INTERVAL_HOURS and retains the newest KEEP_SNAPSHOTS. In the included Compose topology, snapshot files are bind-mounted under:
/root/memory/qdrant_snapshots
Copy snapshots to independent storage; local retention is not an off-site backup.
Restore is an operator-controlled Qdrant action. Stop application writes first, select a verified snapshot, and follow the Qdrant recovery procedure appropriate to the deployed version. Verify collection counts and application health before re-enabling writes.
New facts use IDs derived from namespace and exact text. Legacy IDs remain readable. The migration command is dry-run by default:
docker compose exec memory-mcp /personal-memory-migrate-idsReview collisions and invalid before apply mode. Apply is an exclusive maintenance operation:
- create and verify a recent Qdrant snapshot;
- stop
memory-mcpand every other writer to thememorycollection; - keep writers stopped until migration completes;
- run the exact deployed image on the same Docker network.
APP_IMAGE='ghcr.io/dzarlax-ai/personal-memory:sha-REPLACE_WITH_DEPLOYED_COMMIT'
docker stop memory-mcp
docker run --rm --network traefik \
--entrypoint /personal-memory-migrate-ids \
"$APP_IMAGE" \
-qdrant-url http://memory-qdrant:6333 \
-apply -confirm-writes-stoppedThe command refuses unconfirmed apply mode. It compares complete normalized payloads and vectors, writes the target before deleting legacy sources, leaves conflicts untouched, and is safe to resume while writers remain stopped.
The repository pins TEI, Qdrant, Docker base images, the embedding model revision, and browser-asset checksums. CI publishes application tags including immutable sha-<short> tags.
The checked-in Compose file uses the moving application tag latest as a convenient baseline. Production deployment should replace it with a tested sha-<short> tag or image digest through a separately reviewed deployment change.
After every deployment, verify:
docker compose ps
curl -fsS https://mcp.example.com/health
docker compose logs --tail=200 memory-mcpAlso verify authenticated memory access, optional routes, snapshot status, and indexing behavior relevant to the enabled features.
This repository does not currently include a license or a contribution policy. Source visibility alone does not define permission to copy, modify, or redistribute the project. Add and review those files before presenting the project as licensed open source or accepting external contributions.