Skip to main content
Glama

XMEMORY

CI License: MIT

A locally hosted, unified cross-referencing memory store bridging multiple AI environments (Claude Desktop, Cursor, Kiro, Codex CLI, OpenCode, Gemini) with a dark-mode dashboard for human management. See SSOT.md for the full architecture spec and design notes (retention, hybrid search, injection sanitization, scoping, write-time dedup, token budgets, contradiction detection — all implemented).

XMEMORY Memory Manager dashboard, showing filterable memory entries with domain, scope, tags, and source columns

What's here

File

Purpose

xmemory_db.py

Shared data layer: schema, embeddings, CRUD, hybrid (vector+FTS5) search, retention, backup/export/import, config

init_db.py

Creates/upgrades xmemory.db (WAL mode, core_memory + vec_memory + fts_memory)

server.py

MCP server (stdio) exposing 8 tools (see "MCP tools reference" below); sanitizes retrieved content

dashboard.py

FastAPI backend serving the dashboard UI + /api/* routes

templates/index.html, static/

Dark-mode dashboard UI (Memory Manager, Add Memory, Search Tester, Conflicts, Backup, Settings)

config.json

Runtime settings: k_limit, token_budget_per_search, log_level, embedding_model

backups/

Generated (gitignored) - timestamped xmemory.db/config.json snapshots from the Backup tab or create_backup

startup.bat

Launches the MCP server and the dashboard together

dashboard_control.bat

Double-click CLI menu to start/stop/restart/monitor just the dashboard

dashboard_ctl.ps1

PowerShell backend for dashboard_control.bat (process tracking, health check, log tailing)

requirements.txt

Top-level dependencies (requirements-lock.txt is the full frozen set that was verified to work)

import_sources.py

Re-runnable importer: pulls memories from Claude Code CLI and Codex CLI into XMEMORY (read-only on its sources)

retention.py

Manual/scheduled maintenance: archives stale, unreused memory-domain rows (dry-run by default, nothing auto-deleted)

docs/xmemory_agent_rules.md

Source of truth for agent behavior instructions - see "Agent instruction layer" below

AGENTS.md, CLAUDE.md + .claude/xmemory_rules.md, .opencode/skills/xmemory.md, docs/chatgpt-desktop-instructions.md

Per-client copies of those instructions, auto-discovered by each tool's own convention

tests/smoke_test.py

End-to-end test suite (store/search/dedup/scoping/conflicts/backup/export/import), self-cleaning, run by CI on every push and locally with venv\Scripts\python tests\smoke_test.py

Related MCP server: Cortex

Installation and setup

Prerequisites

  • Windows 11 (this build's target; see SSOT.md). The Python code itself is cross-platform, but dashboard_control.bat/dashboard_ctl.ps1 and startup.bat are Windows-specific launchers.

  • Python 3.11+ on PATH (python --version). Tested against 3.11.

  • ~1 GB free disk for the venv + the nomic-ai/nomic-embed-text-v1.5 embedding model (~550MB, downloaded once, cached locally, then used offline).

Step by step

  1. Get the code.

    git clone https://github.com/<your-username>/xmemory.git C:\GIT\XMEMORY
    cd C:\GIT\XMEMORY

    (Or just use whatever local copy you already have at C:\GIT\XMEMORY — that path is baked into dashboard_control.bat/dashboard_ctl.ps1's comments and this README's examples, but every script actually resolves its own location at runtime, so a different path works fine too.)

  2. Create the virtual environment and install dependencies.

    python -m venv venv
    venv\Scripts\pip install --upgrade pip
    venv\Scripts\pip install -r requirements.txt

    This installs mcp, sqlite-vec, sqlcipher3-wheels (SQLCipher, for database encryption at rest — ships a prebuilt Windows wheel, no compiler needed), keyring (OS-native key storage), sentence-transformers (pulls in torch, the largest download here), fastapi, uvicorn, jinja2, and python-multipart. requirements-lock.txt is the exact frozen set verified to work together, if you hit a version conflict with plain requirements.txt.

  3. Initialize the database.

    venv\Scripts\python init_db.py

    Creates xmemory.db in WAL mode with core_memory (metadata), vec_memory (sqlite-vec, 768-dim embeddings), and fts_memory (FTS5 keyword index). Safe to re-run — it only creates what's missing and migrates an older schema forward (see xmemory_db.init_schema()). The file is SQLCipher-encrypted automatically from creation — a random key is generated and stored via the OS keyring on first run, with no extra step. See "Data protection: encryption and secret scanning" below for what this covers and how to back up the key. (If you have an existing pre-encryption xmemory.db from an older XMEMORY install, run venv\Scripts\python encrypt_db.py once instead of init_db.py — it migrates it in place, safety-backing up the plaintext original first.)

  4. Verify it actually works before wiring anything up:

    venv\Scripts\python -c "import xmemory_db as db; mid = db.store_memory('fact', 'setup-check', 'XMEMORY installed correctly.', 'setup'); print(db.hybrid_search('installed correctly')); db.delete_memory(mid)"

    You should see a short list of search results printed (the memory you just stored, ranked by relevance — you'll see others too if you've already imported data), then no errors. This exercises the full path: sqlite-vec extension loading, embedding model download-or-load, FTS5 indexing, hybrid search, and delete. The first run downloads the embedding model (a minute or two depending on your connection); every run after that is fully offline.

  5. Start it. See "Running everything" below for startup.bat (MCP server + dashboard together) or dashboard_control.bat (dashboard-only control panel). Either way, the dashboard ends up at http://127.0.0.1:8765.

  6. (Optional) Bring in memories that already exist elsewhere on this machine. See "Importing from other AI tools" below for the two built-in sources (Claude Code CLI, Codex CLI), or hand IMPORT_ENV_AI.md to an AI agent to survey the machine more broadly and import what it finds, with your confirmation at each step.

  7. (Optional) Wire XMEMORY into your AI tools. See "Integration" below for exact config snippets per tool (Claude Desktop, Cursor, Kiro, Claude Code CLI, Codex CLI, ChatGPT Desktop, OpenCode, and generic stdio-MCP agents).

Troubleshooting

  • ModuleNotFoundError for mcp.server.fastmcp — you have mcp 2.x installed (FastMCP was renamed to MCPServer); server.py already uses the new import, so if you see this from your own code, update it the same way.

  • TypeError: unhashable type: 'dict' from the dashboard — a Starlette/Jinja2 version mismatch in TemplateResponse call order; dashboard.py already uses the current (request, name, context) form.

  • Port 8765 already in usedashboard_ctl.ps1 -Action start refuses to start if something else is already listening there; check Get-CimInstance Win32_Process -Filter "Name='python.exe'" for a stray process, or change the port in dashboard_control.bat/dashboard_ctl.ps1 and startup.bat together (all three currently hardcode 8765).

  • sqlite3.OperationalError: no such module: vec0sqlite_vec.load() didn't run, usually because you're using a python.exe outside the venv (system Python may not allow extension loading, or won't have sqlite-vec installed at all). Always use venv\Scripts\python.exe.

Running everything

startup.bat

This launches:

  1. server.py in its own console window (stdio MCP server — see note below).

  2. The dashboard at http://127.0.0.1:8765.

You can also run each piece manually:

venv\Scripts\python server.py
venv\Scripts\python -m uvicorn dashboard:app --host 127.0.0.1 --port 8765

Note on the MCP server window: server.py speaks MCP over stdio. In real usage, each client (Claude Desktop, Cursor, Kiro, ...) spawns its own python server.py process per the config below and talks to it directly over stdio — they do not connect to the window startup.bat opens. That window is included only so startup.bat satisfies "launch both at once" for manual/standalone testing; it's safe to close if you're only using the dashboard, and safe to leave running otherwise (idle, waiting on stdin).

Dashboard control panel

Double-click dashboard_control.bat for an interactive menu to start, stop, restart, and monitor the dashboard without touching the MCP server:

[1] Start dashboard        [5] View recent logs
[2] Stop dashboard         [6] Live tail logs (press any key to return)
[3] Restart dashboard      [7] Open dashboard in browser
[4] Refresh status         [0] Exit

It runs the dashboard as a hidden background process, tracks it via dashboard.pid, and logs to dashboard.out.log / dashboard.err.log. Closing the menu does not stop the dashboard — use option [2] first if you want it down. It refuses to start if port 8765 is already held by an unrelated process, and it won't double-start if it's already running.

Importing from other AI tools

import_sources.py pulls existing memories from two sources already found on this machine:

  • Claude Code CLI~/.claude/projects/*/memory/*.md (its own per-project memory files).

  • Codex CLI~/.codex/memories_1.sqlite (stage1_outputs table, Codex's own curated session-memory pipeline), opened via a read-only URI connection so the live file is never locked or written.

Both sources are only ever read — nothing in ~/.claude or ~/.codex is modified. Claude Code's memory type field (user/feedback/project/ reference) is normalized into XMEMORY's domain vocabulary so the dashboard's domain filter stays meaningful — user/referencefact, feedbackrule, projectmemory; the original type is preserved as a type:<original> tag either way. Codex's session recaps import as memory. See the module docstring in import_sources.py for the exact mapping and field provenance.

The script is safe to re-run: every imported row carries a src:<tool>:<id> tag, checked before insert, so running it again after new Claude Code / Codex memories accumulate only imports what's new.

venv\Scripts\python import_sources.py --dry-run   # preview counts, no writes
venv\Scripts\python import_sources.py              # actually import

As of the last run: 75 Claude Code memories + 41 Codex memories = 116 rows imported, breaking down as:

domain

count

source_agent

count

memory

97

claude-code

75

rule

10

codex-cli

41

fact

9

Hybrid search: vector + keyword fusion

hybrid_search() runs two rankings over the whole corpus and fuses them with Reciprocal Rank Fusion (RRF) before applying the k cutoff:

  • Vector KNN (vec_memory, cosine similarity) — catches semantic matches even when the wording differs from what's stored.

  • FTS5 keyword search (fts_memory, SQLite's built-in full-text index) — catches exact-term matches (hostnames, error strings, package names) that pure embedding similarity is often weak on.

Each result carries both similarity (cosine) and fused_score (the RRF total); the dashboard's Search Tester shows whichever is more informative per result. Domain/tag filters and the archived flag are applied as a candidate set intersected with the fused ranking, not a pre-filter on one side only — see xmemory_db.hybrid_search()'s docstring for the exact mechanics.

Embedding model

The default is nomic-ai/nomic-embed-text-v1.5 (Apache 2.0, 768-dim, trust_remote_code=True), loaded via sentence-transformers and cached locally after the first run (~523MB on disk). It replaced the original default, all-MiniLM-L6-v2 (384-dim), because MiniLM hard-truncates input at 256 tokens (~1000 characters) with no warning — a check against a 106-row sample of real stored memories found ~90% exceeded that limit (median content length ~3112 characters), meaning most memories were being silently embedded on a truncated prefix only. nomic-embed-text-v1.5 supports up to 8192 tokens, comfortably covering realistic memory content.

It uses asymmetric task prefixes per its training convention — search_document: is prepended to content being stored/compared, search_query: to search strings — handled automatically by xmemory_db.embed_text()'s task parameter; nothing an operator needs to manage. It requires transformers<5.17 (a later release removed an attention-mask helper the model's trust_remote_code modeling code still calls — pinned in requirements.txt) and einops.

Switching embedding_model in config.json to a different model requires re-embedding every stored memory, since old vectors from one model aren't comparable to a different model's vector space, and vec_memory's column is fixed-dimension (a dimension change can't be applied in place). Run:

venv\Scripts\python reembed.py

after changing the config value. It takes its own safety backup first, computes every new vector before touching the database, then rebuilds vec_memory from scratch. Pass --yes to skip the confirmation prompt.

Retrieved-content safety (MCP tool only)

server.py's hybrid_search MCP tool treats retrieved memory content as untrusted before handing it back to a consuming agent: known prompt-injection patterns (<|...|> role markers, [INST]/[SYSTEM] tags, <system>/<assistant>/<user> pseudo-tags, line-start SYSTEM:/ASSISTANT:/USER: prefixes) are neutralized, each result is capped at 1500 characters, and every result's content is prefixed with [xmemory#<id>] so a consuming agent can tell "retrieved memory" apart from live instructions. This only applies to the MCP tool — the dashboard's /api/search is human-facing and shows content unmodified.

Data protection: encryption and secret scanning

Two independent layers: one keeps secrets out of the database in the first place, the other protects whatever content is stored.

Encryption at rest. xmemory.db is encrypted with SQLCipher (AES-256, whole-database, applied below SQLite's query engine) — hybrid_search, FTS5 keyword search, and vector search all work exactly as before, since SQLCipher decrypts pages transparently before SQLite ever sees them. A copied xmemory.db or backups/*.db file is unreadable without the matching key — that's the point, but it also means:

  • The key is everything. It's generated once and stored via keyring (Windows Credential Manager/DPAPI on this platform, never a plaintext file). Back it up before you need it: venv\Scripts\python manage_key.py show prints it for you to save somewhere safe (a password manager, a printed recovery sheet). manage_key.py import restores a previously-exported key on a different machine/account, or after this one's keyring is lost. There is no other recovery path — losing the key means losing every backup made with it, permanently. This is a local-only tool with no server-side escrow.

  • Headless/CI/containers have no OS keyring session. Set the XMEMORY_DB_KEY environment variable (64 hex characters) and xmemory_crypto.py uses it instead of touching keyring at all — checked first, every time, so it also works as a deliberate override on a normal desktop (e.g. injecting the key from an external secrets manager).

  • Backups from before this feature (an older XMEMORY install) are plaintext; the dashboard's Backup tab and list_backups() still show them, but restore_backup() refuses to restore one over the live (encrypted) database, since that would silently downgrade it back to plaintext.

Write-time secret detection. store_memory/update_memory scan new content for known credential shapes (AWS/GitHub/Slack/Stripe/Google keys, PEM private key blocks, JWTs, an assigned api_key/password/token) before storing it, and reject the write if one matches — every client (MCP tools, the dashboard, bulk import) goes through the same two functions, so this is enforced uniformly, not just an instruction an agent might forget. Like write-time deduplication, it never silently blocks or silently strips content: it raises an error with a redacted preview so you can judge it, and force=True (the same flag that already overrides a duplicate warning) stores it anyway for a deliberate false positive (e.g. a revoked example key in documentation). A secondary check for generic high-entropy tokens exists but is off by default (Settings tab, or config.json's secret_scanning.entropy_check_enabled) — it catches unrecognized random-looking secrets at the cost of flagging a lot of ordinary technical content (filenames, code identifiers); the pattern-based checks above stay active either way and had zero false positives against this project's own real memory corpus.

Retention: archiving stale memories

Finds memory-domain rows that are both old (default: 90+ days since updated_at) and rarely/never reused (default: matched by hybrid_search fewer than 1 time, tracked via match_count/last_matched_at), and archives them — excluded from search, not deleted. Two equivalent ways to run it, sharing the same xmemory_db.py implementation (find_retention_candidates/archive_many/purge_archived):

  • CLI (retention.py) — dry-run by default, nothing changes until you pass --apply:

    venv\Scripts\python retention.py                          # preview candidates
    venv\Scripts\python retention.py --apply                  # archive them
    venv\Scripts\python retention.py --archive-after-days 30 --min-match-count-to-keep 2
    venv\Scripts\python retention.py --purge                  # preview already-archived rows
    venv\Scripts\python retention.py --purge --apply          # permanently delete them
  • Dashboard (Backup tab's Retention panel) — set the same two thresholds, "Find Candidates", review the list (each row expandable, all pre-checked), deselect any you want to keep, "Archive Selected" (confirms first). No dashboard UI for --purge yet — permanent deletion of already-archived rows is CLI-only for now, a deliberate extra step of friction for an irreversible action.

Archived rows can also be managed from the dashboard's Memory Manager tab (Archive/Restore buttons per row, "Show archived" toggle) regardless of which path archived them. rule/fact rows are never touched by retention — only episodic memory-domain rows decay this way, since standing rules and facts don't go stale on a schedule.

Global / local / project-aware scoping

core_memory.scope is 'global' (applies everywhere) or a project slug. It's not a hard filter — hybrid_search's project parameter (MCP tool) or the Search Tester's "Project scope" field gives same-project and global memories a small ranking boost, so they surface ahead of other-project noise without hiding a genuinely strong match from elsewhere. Set it per memory via the Add Memory form's Scope field (defaults to global) or the Memory Manager's Edit action; filter the grid by scope via the toolbar dropdown.

Write-time duplicate detection

store_memory/update_memory check new content against existing active same-domain memories before writing; a ≥0.90 cosine-similarity match is rejected with a pointer to the existing memory's id instead of being stored, preventing the corpus from filling up with near-identical restated facts over time. Override with force=true (MCP tools) or the dashboard's confirm-to-override prompt when you genuinely want both.

Possible duplicates / conflicts

The dashboard's Conflicts tab surfaces same-domain, same-scope memory pairs whose content is highly similar (cosine ≥ 0.90) but weren't caught at write time (e.g. two separately-imported memories, or two that drifted apart in scope before growing similar again) — detection only, nothing resolves automatically. Pick which side of a pair stays current; the other gets archived and the kept memory's supersedes field records the replacement, so nothing is silently lost. Not exposed as an MCP tool on purpose — resolving a contradiction is a human call.

Token-budget-aware search results (MCP tool only)

On top of the per-result 1500-character cap and the k result-count cap, hybrid_search's MCP tool now also caps the total approximate token cost across all returned results combined (config.json's token_budget_per_search, default ~2000, editable from the dashboard's Settings tab). Results are added in ranked order until the next one would exceed budget, then stop — except the single best result is always included even if it alone exceeds budget, so a real match is never withheld outright.

Backup, export, and import

Two different mechanisms, for two different needs:

  • Backup/restore (dashboard's Backup tab, or the create_backup MCP tool) — a full raw snapshot of xmemory.db (WAL-checkpointed first)

    • config.json, byte-for-byte, including vectors and the FTS5 index. This is disaster recovery: "put everything back exactly as it was." Timestamped into backups/. Creating a backup is non-destructive (only ever adds a file) and available as an MCP tool. Restoring overwrites the live database — dashboard-only, never an MCP tool — and always takes its own safety backup of current state first, so a mistaken restore is itself undoable. The dashboard also lets you download a backup file directly.

  • Export/import (dashboard's Backup tab, or the export_memories/ import_memories MCP tools) — a portable JSON format of memory content (domain, tags, content, source_agent, scope, updated_at — no vectors, those get regenerated on import via the normal embedding path). This is for moving or merging a subset of memories: between machines, between embedding models, or just to inspect/share what's stored. Export takes the same domain/tags/scope/include_archived filters as search. Import runs every record through the normal store_memory path, so the same ≥0.90 dedup check applies — duplicates are skipped and reported, not silently re-added (pass force to bypass). Import is purely additive; it never deletes or overwrites existing memories.

:: backup (dashboard-equivalent, or use the Backup tab)
venv\Scripts\python -c "import xmemory_db as db; print(db.create_backup())"

The dashboard's Backup tab covers all of this without touching a terminal: create/list/download/restore backups, and export-with-filters / import-from-file forms with a result summary (imported / skipped duplicates / failed, with reasons).

Verified behavior

The dashboard and DB layer were smoke-tested end-to-end during implementation: store → hybrid search (vector+FTS5 fusion, with similarity and fused scoring) → update (re-embeds and re-indexes automatically) → delete (removes the core_memory, vec_memory, and fts_memory rows) → archive/restore → backup/restore (including the Windows WAL-file-lock retry path) → export/import (including the dedup interaction) → settings read/write. All confirmed working against this repo's venv, both via direct Python calls and through the actual dashboard UI in a real browser (including the file-upload import flow, using a synthetic File/ DataTransfer object). import_sources.py was run in --dry-run mode first to verify counts/mapping, then for real, then re-run to confirm it's idempotent (0 new rows on the second pass). The schema migration (adding archived/match_count/last_matched_at/scope/supersedes columns and the fts_memory table, then backfilling FTS5 and scope for the existing 116 rows) was verified against the live database, not just a fresh one. retention.py was run in dry-run mode against the real dataset. The MCP server's injection sanitizer was verified against a crafted <|im_start|>system...<|im_end|> payload. Write-time dedup was verified for identical content (rejected), genuinely different content (accepted), and the force override (bypasses correctly). Conflict detection/ resolution was verified against both a crafted near-duplicate pair and the real corpus. Scoping was verified live: a project-scoped search correctly re-ranked matching-scope results ahead of others. All of the above were also exercised through the actual dashboard UI (not just the Python API) in a real browser, including catching and fixing a UI bug along the way (the Add Memory scope field's pre-filled default value would silently concatenate with typed input instead of being replaced — fixed to use a placeholder instead).

tests/smoke_test.py now codifies the core of this into a repeatable, self-cleaning suite (store/search/delete, dedup rejection + force override, scope-bonus ranking, conflict detection + resolution, backup + restore round-trip, export + import round-trip) that CI runs on every push. Building it surfaced one genuine, worth-knowing scoping nuance: the project ranking bonus applies to every global-scoped row, not just relevant ones — so in a populated corpus, a row scoped to a specific different project can legitimately rank below unrelated global content in a small top-k window, even when it's semantically closer to the query. That's consistent with the intended design (prefer your own project + global defaults over a different specific project), not a bug, but it's worth knowing if a search from inside a project feels like it's surfacing "unrelated global stuff" ahead of a highly specific match from another project.

Agent instruction layer

Wiring the MCP server into a client (see "Integration" below) makes the tools available, but doesn't tell an agent when or how to use them well — that's a separate, smaller problem worth solving explicitly, since a tool an agent doesn't reliably reach for is close to useless.

docs/xmemory_agent_rules.md is the single source of truth for that behavior guidance. Everything else is a generated copy, placed wherever each client auto-discovers instructions by its own convention, so nothing extra needs configuring beyond the MCP wiring itself:

File

Auto-discovered by

AGENTS.md (repo root)

Codex CLI, and the general AGENTS.md convention several other tools follow

CLAUDE.md (repo root)

Claude Code CLI — a 3-line pointer using Claude Code's @path file-import syntax to pull in .claude/xmemory_rules.md

.claude/xmemory_rules.md

The file CLAUDE.md imports — same content as AGENTS.md, kept as its own file so the import target is unambiguous

.opencode/skills/xmemory.md

OpenCode's skills mechanism (has YAML frontmatter name/description OpenCode uses to decide when to surface it)

docs/chatgpt-desktop-instructions.md

Nothing automatic — ChatGPT doesn't have a file-based auto-discovery convention; paste this into Custom Instructions manually. See "ChatGPT Desktop" under Integration for the separate (and non-obvious) question of how the connection itself works.

The core instructions (a "Universal Artifact" — Search-before-acting, project-scoped search, dedup/duplicate handling, the real domain vocabulary, and memory-poisoning/conflict-resolution security guardrails) are kept byte-identical across AGENTS.md, .claude/xmemory_rules.md, and .opencode/skills/xmemory.md on purpose: an exact-match static prefix is what makes prompt caching effective, so this text shouldn't be reworded or reordered per-client. If you edit the behavior guidance, edit docs/xmemory_agent_rules.md first, then propagate the same change identically into the three artifact copies and into docs/chatgpt-desktop-instructions.md's paraphrased version.

Active learning (opt-in, off by default)

config.json's active_learning block is a feature flag, not new code — when enabled: true, agents following the instruction layer above inject an extra "ask before storing if uncertain" protocol into their own behavior (human-in-the-loop confirmation for borderline memories). No new MCP tool: the gating is instruction-driven client-side, specifically to avoid growing the tool surface (more tools = more prompt-prefix cost = a worse-caching prefix). See docs/xmemory_agent_rules.md's "Active Learning Protocol" section for the exact injected text and rollout guidance, and "Configuration" below for the schema.

MCP tools reference

server.py exposes 8 tools over stdio. All write tools return a compact JSON string (never raise on expected conditions like a duplicate or a missing id - errors come back as {"status":"error"/"duplicate", ...} so an agent can branch on them without a try/catch). Three tools deliberately have no MCP equivalent — see "Why some operations are dashboard-only" below.

Tool

Parameters

Returns

Notes

store_memory

domain, tags, content, source_agent, force=False

{"id", "status":"stored"}, {"status":"duplicate", "existing_id", "similarity", "detail"}, or {"status":"secret_detected", "label", "redacted", "detail"}

Embeds content, indexes it into FTS5, defaults scope to 'global' (use update_memory after to set a project scope). Rejects ≥0.90 cosine-similar existing content, and content matching a known credential shape (see "Data protection" above), unless force=True (overrides both).

hybrid_search

query, domain="", tags="", project=""

JSON array of {"id","domain","tags","content","source_agent","scope","score"}

Vector+FTS5 RRF fusion. project nudges same-project/global results ahead, doesn't filter. Capped by k_limit (config, default 5) and token_budget_per_search (config, default ~2000 approx tokens) — may return fewer than k results if budget runs out, but always at least 1 if there's a match. Each result's content is injection-sanitized, 1500-char capped, and prefixed [xmemory#<id>]. Matched rows' match_count/last_matched_at are bumped (used by retention.py).

update_memory

id, domain="", tags="", content="", source_agent="", scope="", supersedes=0, force=False

{"id", "status":"updated"} or a "duplicate"/"secret_detected"/"error" status

Only non-empty/non-zero fields change. Re-embeds and re-indexes FTS5 only if content changes (and only then runs the dedup and secret-detection checks). supersedes marks this memory as replacing another by id — does not archive the replaced one itself (use the dashboard's Conflicts tab for that combined action, or archive_memory isn't exposed to MCP - do it from the dashboard).

delete_memory

id

{"id", "status":"deleted"}

Removes the core_memory, vec_memory, and fts_memory rows. Not soft — there's no MCP-exposed archive/restore; use delete_memory sparingly and lean on the dashboard's Archive button for anything you might want back.

list_memories

(none)

JSON array of all active memories (metadata + content, no vectors)

No pagination/limit - fine at hundreds of rows, will need one if the corpus grows to many thousands.

export_memories

domain="", tags="", scope="", include_archived=False

JSON array (no vectors)

Read-only. Same filters as search; use to hand a subset of XMEMORY to another tool/process or inspect current contents.

import_memories

data (JSON string, array of objects with at least content), force=False

{"imported", "skipped_duplicates", "failed"}

Purely additive - never overwrites/deletes. Each record goes through store_memory's normal embed+dedup+secret-detection+index path. A duplicate is silently skipped and counted; a detected secret lands in failed with the reason. force=True bypasses both checks for the whole batch.

create_backup

(none)

{"backup_file", "config_file", "size_bytes", "row_count", "created_at"}

Non-destructive snapshot of xmemory.db+config.json into backups/. Good practice to call before a large import_memories batch.

Why some operations are dashboard-only

Three operations exist in xmemory_db.py and the dashboard, but are not MCP tools, on purpose — each overwrites or resolves something in a way that should be a human decision, not something an unattended agent triggers:

  • restore_backup — overwrites the entire live database. An agent accidentally (or via a confused/adversarial prompt) restoring a stale backup would silently roll back everything written since. Dashboard's Backup tab only, and it double-confirms (browser confirm(), plus its own automatic safety-backup-before-restoring).

  • resolve_conflict — decides which of two contradictory memories is correct and archives the other. Detecting a possible contradiction is cheap and safe to automate (that's what find_possible_conflicts / the Conflicts tab do); resolving one requires judgment about which claim is actually true, which is exactly the kind of thing this project doesn't want an agent silently doing to its own knowledge base.

  • archive_memory/restore_memory (per-row, not the DB-wide restore above) — not currently exposed either; still small/reversible enough that this is more a consistency choice (keep row-level curation actions dashboard-only alongside the two above) than a hard safety requirement. Worth revisiting if an agent-driven retention workflow becomes useful later.

Integration

Recommended for every client below: set PYTHONUNBUFFERED=1 in the server's environment. Python buffers stdout by default when it's not attached to a terminal (i.e. always, for a stdio MCP subprocess), which can delay JSON-RPC responses reaching the client; combined with server.py's own UTF-8 stdio hardening (Windows defaults stdout/stderr to the locale codepage, e.g. cp1252, which can corrupt UTF-8 payloads), this keeps the stdio transport reliable on Windows. The JSON snippets below include it; add the equivalent for any client not shown.

Claude Desktop

Edit %APPDATA%\Claude\claude_desktop_config.json and add an entry under mcpServers:

{
  "mcpServers": {
    "xmemory": {
      "command": "C:\\GIT\\XMEMORY\\venv\\Scripts\\python.exe",
      "args": ["C:\\GIT\\XMEMORY\\server.py"],
      "env": { "PYTHONUNBUFFERED": "1" }
    }
  }
}

Restart Claude Desktop after saving. All 8 tools from the "MCP tools reference" above (store_memory, hybrid_search, update_memory, delete_memory, list_memories, export_memories, import_memories, create_backup) will appear as available MCP tools.

Claude Code CLI

Distinct from Claude Desktop above — this is the CLI tool. Easiest is the official command (run from anywhere, registers it at user scope so it's available in every project):

claude mcp add --scope user -e PYTHONUNBUFFERED=1 xmemory -- C:\GIT\XMEMORY\venv\Scripts\python.exe C:\GIT\XMEMORY\server.py

Or edit the config JSON directly — Claude Code CLI reads MCP servers from ~/.claude/mcp.json (global) or a project-local .mcp.json in a repo root (same mcpServers schema as Claude Desktop above):

{
  "mcpServers": {
    "xmemory": {
      "command": "C:\\GIT\\XMEMORY\\venv\\Scripts\\python.exe",
      "args": ["C:\\GIT\\XMEMORY\\server.py"],
      "env": { "PYTHONUNBUFFERED": "1" }
    }
  }
}

Run claude mcp list to confirm it's registered, then restart/reload any running claude session to pick it up.

Cursor

Cursor reads MCP servers from .cursor/mcp.json (project-level) or the global ~/.cursor/mcp.json. Add:

{
  "mcpServers": {
    "xmemory": {
      "command": "C:\\GIT\\XMEMORY\\venv\\Scripts\\python.exe",
      "args": ["C:\\GIT\\XMEMORY\\server.py"],
      "env": { "PYTHONUNBUFFERED": "1" }
    }
  }
}

Kiro

In Kiro's MCP settings (Kiro > Settings > MCP Servers, or the workspace .kiro/settings/mcp.json), add the same command/args/env as above under a xmemory key.

OpenCode — already wired up

%APPDATA%\opencode\opencode.json has an xmemory entry under mcp (alongside the pre-existing tolaria and open-knowledge servers, both left untouched):

"xmemory": {
  "type": "local",
  "enabled": true,
  "command": [
    "C:\\GIT\\XMEMORY\\venv\\Scripts\\python.exe",
    "C:\\GIT\\XMEMORY\\server.py"
  ],
  "environment": { "PYTHONUNBUFFERED": "1" }
}

A backup of the pre-edit config was saved alongside it (opencode.json.bak-xmemory-<timestamp>). Restart OpenCode to pick it up. Re-verified 2026-09-17: the config still points at real, current files (venv\Scripts\python.exe and server.py both exist), server.py starts cleanly with no errors, and all 8 tools (including the newer export_memories/import_memories/create_backup) are registered on it — no drift since the original wiring. PYTHONUNBUFFERED=1 was added to the live config as part of this same pass (it had been recorded as done in an earlier memory entry that turned out not to match reality — fixed both the config and the memory).

Codex CLI

Codex CLI uses TOML, not JSON, and its command/args split the executable and its arguments differently — add a [mcp_servers.xmemory] table to ~/.codex/config.toml:

[mcp_servers.xmemory]
command = "C:\\GIT\\XMEMORY\\venv\\Scripts\\python.exe"
args = ["C:\\GIT\\XMEMORY\\server.py"]

[mcp_servers.xmemory.env]
PYTHONUNBUFFERED = "1"

Run codex mcp list to confirm it's registered, and restart any running Codex session to pick it up.

ChatGPT Desktop

Two different integration paths, and they're not interchangeable:

  • If you use ChatGPT Desktop in its Codex-connected/agent mode, it reads MCP servers from the same ~/.codex/config.toml as Codex CLI above — the [mcp_servers.xmemory] entry you just added already covers it, nothing extra to configure.

  • If you use ChatGPT's web-style "Developer Mode" custom connectors (Settings → Apps & Connectors → Advanced Settings → Developer Mode → Create), that path is remote-HTTPS-and-OAuth only — it does not connect to a local stdio process the way Claude Desktop/Cursor/Codex CLI do. server.py only implements stdio transport today, so it can't be wired into a Developer Mode connector without additional work: exposing it over streamable-http (the mcp package supports this transport; server.py would need a code change to use it instead of stdio) and a tunnel (e.g. Cloudflare Tunnel, ngrok, or OpenAI's own Secure MCP Tunnel) to make that endpoint reachable from OpenAI's servers, plus OAuth. That's real added infrastructure for a tool meant to stay local and single-machine (see SSOT.md) — not done here, and not recommended unless you specifically need ChatGPT's web/mobile clients (not the desktop app) to reach XMEMORY too.

Gemini CLI / other stdio-MCP agents

Any other agent that supports stdio MCP servers can attach the same way: point its MCP config at command: C:\GIT\XMEMORY\venv\Scripts\python.exe with args: ["C:\GIT\XMEMORY\server.py"] (or the TOML equivalent if it follows Codex CLI's config style). For Dockerized agents, volume-map C:\GIT\XMEMORY into the container so the agent can reach the shared xmemory.db file and run server.py (with the container's own Python environment, or by also mounting venv), keeping in mind SQLite's WAL mode allows safe concurrent access from multiple processes/containers as long as they all use the same xmemory.db path.

Configuration

config.json controls:

  • k_limit — hard cap on results returned by hybrid_search (default 5, prevents token bloat).

  • token_budget_per_search — approximate total token cap (chars÷4) across all of hybrid_search's MCP-tool results combined (default 2000). See "Token-budget-aware search results" above.

  • log_level — reserved for future logging verbosity control.

  • embedding_model — the sentence-transformers model name (default nomic-ai/nomic-embed-text-v1.5, 768 dims — see "Embedding model" above). Changing this requires re-embedding existing memories: run venv\Scripts\python reembed.py after updating the value, which rebuilds vec_memory for every row with the new model (takes its own safety backup first).

  • active_learning — opt-in feature flag block for the instruction-driven "ask before storing an uncertain memory" behavior (see "Agent instruction layer" above). Default {"enabled": false, "scope": "project", "ask_before_store": true, "max_questions_per_session": 3, "confidence_threshold": 0.8}. server.py logs the effective values at startup; flipping enabled requires no code change, just editing this block (directly, or by asking an agent following docs/xmemory_agent_rules.md to do so) and restarting the MCP server process.

Edit config.json directly, or use the Settings tab in the dashboard (k_limit, token_budget_per_search, and log_levelactive_learning isn't in the dashboard UI yet, edit the file for that one).

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides persistent, local-first AI memory across sessions via MCP tools for storing, searching, and retrieving context from past interactions.
    1
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Local-first AI memory layer with hybrid retrieval and brain-inspired namespaces. Enables agents to save, search, and manage memories directly via MCP tools.
    3 npm
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Provides persistent, searchable memory for AI agents across any MCP-compatible client, storing project context, user preferences, and session learnings locally in SQLite with tools to save, retrieve, search, and manage them.
    12
    12 npm
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides a cross-session memory system for AI assistants via MCP, with markdown-based storage, BM25 retrieval, and tools for writing, searching, reading, and managing memories.
    4 npm
    MIT