Skip to main content
Glama

apex-memory

apex-memory is a knowledge store for AI agents. It keeps markdown notes in a database: SQLite or Postgres. Agents use the store through the Model Context Protocol (MCP).

Each write is one transaction. The store does a check of the knowledge in each write. If the write adds a problem, such as a broken link, the store rolls back the write. The store then tells the agent why.

  • Python package: apex_memory

  • Command-line interface (CLI): apex-memory

  • License: MIT

  • Python: 3.12 or later

The project started as a part of a private project. This repository is the public extract.

Low token cost. In 21 days of real use, the store used less than 1% of all new context tokens. File reads, search and shell output used approximately 15 times more. See Token cost.

Contents

Related MCP server: Zero Brain MCP Server

Quick start for agents

This procedure installs the store with SQLite, creates one domain and connects Claude Code. For other MCP clients, see Connect an MCP client.

  1. Make sure that uv is installed. Run uv --version. If the command fails, install uv from https://docs.astral.sh/uv/.

  2. Install the package from GitHub:

    uv tool install git+https://github.com/Shaurya-GitH/apex-memory
  3. Run apex-memory --help. If the shell cannot find the command, run uv tool update-shell and open a new shell.

  4. Create the store. The default database is the SQLite file ~/.apex-memory/memory.db.

    apex-memory init
  5. Create the first domain. Use the first script in Create a domain. Replace myproject with the name of your domain.

  6. Create the ops domain memory-ops. Use the second script in Create a domain. The store records refusals and errors there.

  7. Do a check of the import:

    apex-memory verify ~/apex-notes/myproject
    apex-memory brief myproject
  8. Register the MCP server for all of your projects. Replace claude-code with a short name for the agent.

    claude mcp add apex-memory --scope user -- apex-memory serve --actor claude-code --role owner
  9. Start a new Claude Code session. Run /mcp and make sure that apex-memory shows as connected.

  10. Copy templates/AGENTS.md into your own AGENTS.md or CLAUDE.md. Replace its placeholders.

  11. In the new session, call the tool brief with domain set to your domain.

Token cost

An agent reads knowledge from the store into its context. Thus the store adds tokens to each session. We measured this cost in real use, not in a benchmark.

The data: all Claude Code sessions of one owner for 21 days. That is 3,515 sessions. 245 of these sessions used the store.

Measure

Result

Store output, as a part of all new context tokens

0.77%

File reads, search and shell output (Read, Grep, Bash), for comparison

11.9%

Store cost in the sessions that used the store, with cache re-reads

3.4% (median 1.7%)

Store output in one session that used the store, on average

approximately 29,000 tokens

Part of the store output that came from read

60%

Part of the store output that came from brief

20%

The store keeps the cost low because an agent reads one section, not a full page. brief gives the agent its start context in one call.

How we measured:

  • We counted the characters in each tool result. We used 4 characters for each token.

  • For cost, we used these weights: input 1, cache write 1.25, cache read 0.1, output 5.

  • For "with cache re-reads", each later turn reads each store result again from the cache, until the context is compacted.

Your results can be different. They change with the size of your knowledge and with how your agents use it.

Data model

The store keeps knowledge in domains. A domain is one area of knowledge, for example one product. This table gives each part of the model.

Item

What it is

Domain

A set of notes with the path prefix <domain>/. Each domain has its own index, status page, log and decision records. You create a domain with apex-memory import.

Note

One markdown page with a path such as <domain>/topics/storage. A note has a title, a summary and a version. A project note also has a page header.

Page header

Three fields on a project note: decision status, implementation status and evidence checked. The store also keeps the date verified_at.

Index

The page <domain>/index. Its catalog table shows one line for each note: the link and the summary. The store makes the catalog from the summaries.

Section

One heading of a note and its text. Each section has an id and a version. You read and edit one section at a time.

Link

[[<domain>/<note>]], [[<domain>/<note>#Heading]] or [[<note>#^block]] in the text. The store keeps the links in the two directions. move rewrites them in notes. The index catalog and the log are generated from rows, so their links are not stored as links. Log entries keep the old page name after a move.

Decision record

A heading such as D01 — Use SQLite (2026-10-30) on the page <domain>/decisions. It has one - **Status:** line.

Decision status

proposed, accepted, rejected or superseded by Dnn. Only set_decision_status changes the Status line.

Status item

One item on the page <domain>/status. The kinds are state, open, blocker and next.

Log

The page <domain>/log. It is append-only. The database stamps the time of each entry.

Tool event

An automatic log record of each write and each search. The log page does not show tool events. log_query shows them on request.

Source

A record of where knowledge came from: snapshot, live, transcript, web or owner. A note can cite a source.

Preference

A rule from the owner about how to do work, with the words of the owner. Each brief shows the active preferences.

Lease

A claim on a section for 1 to 60 minutes. A longer time becomes 60 minutes. Other agents cannot write to that section while the lease is active.

Working note

A scratch note under <domain>/working/<actor>/. It needs no summary and no page header. It is not in the index.

Ops domain

The domain that gets an automatic record of each refusal and each error. The default name is memory-ops.

The check

The store does a check of a domain before and after each write. The check finds these problems:

  • Broken links, and anchors that the store cannot find.

  • Notes that have no summary, so they are not in the index.

  • The same paragraph of 200 characters or more in two notes.

  • Decision records that do not have exactly one Status line.

  • Notes that cite a live source, with a verified_at date older than 14 days.

If a write adds a new problem, the store rolls back the write. The refusal check_failed lists the problems. Problems that were there before the write do not stop it. The tool check and the command apex-memory check show all the problems of a domain.

Decision records

A decision record is a heading on <domain>/decisions. The heading starts with an id: D or P and digits. Then it has — and a title. Write the record as in this example:

## D01 — Use SQLite first (2026-10-30)
- **Status:** proposed
- **Decision:** The store uses SQLite first.

These rules apply to decision records:

  • You cannot delete a record. Supersede it with set_decision_status.

  • Only set_decision_status changes the Status line.

  • You can edit a proposed record. You can only append text to an accepted, rejected or superseded record.

  • The status can change from proposed to accepted, rejected or superseded. It can change from accepted to superseded.

  • superseded_by must name an accepted decision.

Install

The package is not on PyPI. Install it from GitHub. You need Python 3.12 or later.

Need

Command

SQLite only, with uv

uv tool install git+https://github.com/Shaurya-GitH/apex-memory

SQLite and Postgres, with uv

uv tool install "apex-memory[postgres] @ git+https://github.com/Shaurya-GitH/apex-memory"

SQLite only, with pip

pip install git+https://github.com/Shaurya-GitH/apex-memory

SQLite and Postgres, with pip

pip install "apex-memory[postgres] @ git+https://github.com/Shaurya-GitH/apex-memory"

The postgres extra installs psycopg[binary] 3. To add Postgres to an installed tool, run the second uv command with --force.

uv tool install puts the command apex-memory in ~/.local/bin. If that folder is not on your PATH, run uv tool update-shell. Use pip in a virtual environment.

To update the tool, run uv tool upgrade apex-memory.

Choose the database

The store finds its database in this sequence:

  1. The option --url.

  2. The environment variable APEX_MEMORY_URL.

  3. The SQLite file ~/.apex-memory/memory.db.

URL

Database

sqlite:///absolute/path/memory.db

A SQLite file at an absolute path.

sqlite://~/path/memory.db

A SQLite file relative to your home folder.

/absolute/path/memory.db

A bare path is a SQLite file.

postgresql://user:password@host:port/database

A Postgres database. postgres:// also works.

Each command and the MCP server open the database and create or update the schema. The store refuses a database whose schema is newer than the installed package. If that occurs, upgrade the package.

SQLite

SQLite needs no setup. apex-memory init creates the file and its folder. Many processes can use one file at the same time. The store lets only one process write at a time.

apex-memory init --url sqlite:///Users/you/stores/memory.db

The output is store <store id> at <path>. Run init again to see the store id. It does not change a store.

Postgres

  1. Install the package with the postgres extra. See Install.

  2. Start a Postgres server. For a local test server, use Docker:

    docker run -d --name apex-memory-pg -e POSTGRES_PASSWORD=test -p 5432:5432 postgres:17

    If a Postgres server already uses port 5432 on your computer, use a different host port, for example -p 55432:5432. Then use that port in the URLs below.

  3. Create a user. The password of the postgres user is test. psql asks for it.

    psql -h localhost -U postgres -c "CREATE ROLE apex LOGIN PASSWORD 'secret'"

    If psql is not installed, run it in the Docker container. Use this form for each psql command in this section:

    docker exec -e PGPASSWORD=test apex-memory-pg psql -U postgres -c "CREATE ROLE apex LOGIN PASSWORD 'secret'"
  4. Create a database that the user owns. The user must be able to create tables in it.

    psql -h localhost -U postgres -c "CREATE DATABASE apex_memory OWNER apex"
  5. Create the store:

    apex-memory init --url postgresql://apex:secret@localhost:5432/apex_memory
  6. Set the URL for all commands. Put this line in your shell profile:

    export APEX_MEMORY_URL=postgresql://apex:secret@localhost:5432/apex_memory

The URL accepts each libpq parameter. To keep a store in its own schema, create the schema first. Then add the options parameter:

psql -h localhost -U apex -d apex_memory -c "CREATE SCHEMA team_a"
apex-memory init --url 'postgresql://apex:secret@localhost:5432/apex_memory?options=-csearch_path%3Dteam_a'

One schema holds one store. If the password contains special characters, percent-encode them in the URL. The store does not show the password in its output.

If the postgres extra is not installed, a Postgres URL stops with the message postgresql needs the psycopg package: install apex-memory[postgres].

Create a domain

No MCP tool creates a domain. You create a domain when you import a folder of markdown notes. The name of the folder is the name of the domain. Use lowercase letters, digits and hyphens.

This script writes the four start pages of a new domain and imports them. Change DOMAIN and NOTES as necessary.

DOMAIN=myproject
NOTES=~/apex-notes
mkdir -p "$NOTES/$DOMAIN"
cat > "$NOTES/$DOMAIN/index.md" <<EOF
# $DOMAIN index

## Catalog

| Page | Purpose |
|---|---|
| [[$DOMAIN/decisions]] | Decision records |
| [[$DOMAIN/status]] | Current state |
| [[$DOMAIN/log]] | Append-only activity |
EOF
cat > "$NOTES/$DOMAIN/status.md" <<EOF
# $DOMAIN status

## Current state

None.

## Unfinished / open

None.

## Blockers

None.

## Next concrete action

None.
EOF
printf '# %s log\n' "$DOMAIN" > "$NOTES/$DOMAIN/log.md"
printf '# %s decisions\n' "$DOMAIN" > "$NOTES/$DOMAIN/decisions.md"
apex-memory import "$NOTES/$DOMAIN"

The store records refusals and errors only when the ops domain exists. This script creates the ops domain memory-ops:

NOTES=~/apex-notes
mkdir -p "$NOTES/memory-ops"
cat > "$NOTES/memory-ops/index.md" <<'EOF'
# memory-ops index

## Catalog

| Page | Purpose |
|---|---|
| [[memory-ops/log]] | Failures and friction of the memory system |
EOF
printf '# memory-ops log\n' > "$NOTES/memory-ops/log.md"
apex-memory import "$NOTES/memory-ops"

To use a different name for the ops domain, give --ops-domain <name> to apex-memory serve.

After the import, the store holds the knowledge. The folder is only the start point. Agents add notes with create_note.

Connect an MCP client

The MCP server uses stdio. The client starts this command:

apex-memory serve --actor <name> --role <owner or worker>

Option

What it does

--actor <name>

The name of the agent. Use lowercase letters, digits and hyphens. The name is in each log entry and in the path of the working notes.

--role owner, --role worker

The role of the agent. The default for serve is worker. See Roles.

--url <url>

The database. You can also set APEX_MEMORY_URL in the environment of the server.

--ops-domain <name>

The domain that gets the records of refusals and errors. The default is memory-ops.

--run-id <id>

An id for this run. The default is a new UUID. Log entries keep it.

When the client connects, the server sends instructions to the agent. The instructions give the store id, the actor, the role and the main rules.

Claude Code

Use --scope user to register the server for all of your projects. Give the URL with -e:

claude mcp add apex-memory --scope user \
  -e APEX_MEMORY_URL=sqlite:///Users/you/.apex-memory/memory.db \
  -- apex-memory serve --actor claude-code --role owner

For Postgres, set APEX_MEMORY_URL to the Postgres URL. Without -e, the server uses ~/.apex-memory/memory.db.

To remove the server, run claude mcp remove apex-memory --scope user.

Codex

codex mcp add apex-memory --env APEX_MEMORY_URL=sqlite:///Users/you/.apex-memory/memory.db \
  -- apex-memory serve --actor codex --role owner

Other MCP clients

Most clients accept a JSON configuration of this type:

{
  "mcpServers": {
    "apex-memory": {
      "command": "/Users/you/.local/bin/apex-memory",
      "args": ["serve", "--actor", "my-agent", "--role", "owner"],
      "env": { "APEX_MEMORY_URL": "sqlite:///Users/you/.apex-memory/memory.db" }
    }
  }
}

A desktop client can start without your shell PATH. If the client cannot find apex-memory, give the full path. To find the full path, run command -v apex-memory.

Roles

You give the role when you register the server. The store does not authenticate the role. It is for one machine with one owner.

Role

Who

What it can do

owner

The sessions of the owner.

All the tools.

worker

Agents that you run for the owner.

Read and search all the knowledge. Write only its own working notes under <domain>/working/<actor>/. Append log entries.

A worker cannot do these actions:

  • Write a project note, a page header or a section outside its working notes.

  • Add or close a status item.

  • Change a decision status.

  • Add or retire a preference.

  • Promote a working note into project knowledge.

  • Change a source that exists.

The owner promotes a working note with move. The promotion needs a summary and a page header.

Search results do not show the working notes of other actors. To see them, search with a scope in the working folder.

MCP tools

Each tool returns a JSON object. A result has "ok": true. A refusal has "ok": false and an error.

Tool

What it does

brief

Gives the start read for a domain: store identity, number of problems, index, preferences, decisions, open status items and the last log entries.

search

Finds text in notes, sections and log entries. mode is bm25, exact or regex. scope is a domain or folder; empty means all domains.

read

Reads a full note, or one section and its subsections, with ids and versions.

links

Shows the links out of a note, into a note, or both, up to 3 hops.

log_query

Filters log entries by time, op text, page or task id. include_tool adds the tool events.

patch_section

Edits one section with a version check. op is replace, append or prepend.

patch_header

Edits the page header of a note with a version check.

create_note

Creates a note in a domain that exists. It never overwrites.

move

Renames a note in its domain and rewrites all links to it. It also promotes a working note.

status_item

Adds or closes one status item. Owner only.

set_decision_status

Changes the Status line of one decision record. Owner only.

log_append

Appends one entry to the log of a domain. The pages that it names must exist.

preference

Adds or retires a preference of the owner. add needs the words of the owner. Owner only.

source

Adds a source, or cites or uncites a source from a note.

lease

Takes or releases a lease on a section.

check

Shows the problems of a domain. It changes nothing.

How to edit a section

  1. Call read with path and heading. The result gives section_id, version and content.

  2. Change the text of content.

  3. Call patch_section with op: "replace", the new content, section_id, and if_version set to version.

  4. If the result is version_conflict, another agent changed the section. Merge your change into the content of the refusal.

  5. Call patch_section again with if_version set to current_version.

The headings in content are relative to the section. A direct subsection starts with #. replace accepts the same format that read gives.

To add a subsection, use op: "append". prepend adds text before the own text of the section. It cannot add headings.

heading can be the heading text or a list with the full heading path. The path starts at the highest heading in the note text. An imported note usually has an H1, so its path starts with the H1, for example ["myproject status", "Blockers"]. A note from create_note has its title outside the text, so its path starts with its first heading, for example ["Design", "Details"]. If two sections have the same heading, the store gives the candidates. Then use section_id.

The index catalog, the status items and the log entries are generated sections. patch_section refuses them. Use the summary field, status_item and log_append.

Search modes

Mode

Use it for

bm25

Topics. A keyword search with a rank order. Lower scores are better.

exact

Ids, hashes and paths. A substring search that ignores case.

regex

Patterns. Each pattern must compile in Python. See Differences between the backends.

Refusals

A refusal is an expected result. It is not an error of the server. The error field gives the reason. Many refusals also give a hint.

error

Cause

What to do

version_conflict

The section or note changed after you read it.

Merge into the content of the refusal. Try again with current_version.

check_failed

The write adds a problem. problems lists it.

Correct the problem in your text. Try again.

leased

Another agent has a lease on that part.

Wait until expires_at, or ask the holder to release it.

not_allowed

A worker tried to write outside its working notes.

Write under <domain>/working/<actor>/.

owner_only

A worker tried an owner action.

Ask the owner.

unknown_domain

The domain is not in the store.

Use a domain from domains, or import the domain.

not_found, ambiguous_heading

The store cannot find the note or section, or the heading is not unique.

Use a heading path or a section_id.

summary_required, header_required

A project note needs a summary and a full page header.

Give summary, decision_status, impl_status and evidence.

exists

The note exists. create_note does not overwrite.

Use patch_section.

generated_section

The store makes the section from rows.

Use status_item, log_append or the summary field.

status_line_changed, decision_overwritten, decision_removed

The edit changes a decision record in a way that is not permitted.

Use set_decision_status, or append text only.

unknown_pages

log_append names a page that is not in the store.

Name pages that exist.

bad_regex

The pattern does not compile in Python.

Correct the pattern.

headings_not_allowed

prepend text contains a heading.

Use append to add subsections.

bad_transition

set_decision_status asks for a change that the life cycle does not permit, for example accepted to proposed.

Use proposed to accepted, rejected or superseded, or accepted to superseded.

quote_required

preference add has no words of the owner.

Give the quote of the owner.

promotion_needs_summary_and_header

move of a working note into project knowledge has no summary or page header.

Give summary and the header fields.

bad_path

The path is not permitted, for example a move of the index.

Use a path of a normal note.

domain_exists

apex-memory import of a domain that is in the store.

Use a new database or another domain name.

If the ops domain exists, the store records each refusal and each error in it. The record keeps the tool, the arguments and the result. The store replaces the content argument and long text with their length.

CLI commands

The CLI uses the same database rules as the server. All commands accept --url. The CLI acts as the actor cli with the role owner.

Command

What it does

apex-memory init

Creates the store, or shows the store that exists.

apex-memory serve --actor <name> --role <role>

Starts the MCP stdio server.

apex-memory import <folder>

Imports a folder of markdown notes as one domain.

apex-memory import <store>

Copies a full store, such as a SQLite file or a backup, into a new empty store.

apex-memory verify <folder>

Makes sure that each imported page renders back the same as its file.

apex-memory brief <domain>

Prints the brief of a domain.

apex-memory check <domain>

Prints the problems of a domain. The exit code is 1 if there are problems.

apex-memory backup [--to <folder>] [--keep <n>]

Writes a verified backup. See Backups.

Import a folder of markdown notes

apex-memory import <folder> reads each *.md file in the folder and its subfolders. The import only reads the files. The path of a note is <folder name>/<relative path without .md>. The report lists other files in not_imported.

The import gives special treatment to these files:

File

Treatment

index.md

Rows such as | [[<domain>/<note>]] | <summary> | give the summaries. If the rows are a plain table at the end of a section, the store makes that catalog from the summaries. If not, the page stays as written.

status.md

Sections that start with Current state, Unfinished / open, Blockers or Next concrete action become status items. Each bullet or paragraph is one item. None. means no items.

log.md

The last level-2 headings with the format ## 2026-10-01T10:00:00+05:30 — OP become log entries. Older sections stay as written.

decisions.md

Headings such as D01 — Title or P01 — Title become decision records.

sources/*.md

Each note becomes a snapshot source.

A note gets a page header from its first line before the first heading, in this format:

> **Decision status:** accepted **Implementation status:** built **Evidence checked:** 2026-10-30, tests pass

The import accepts simple YAML front matter: key: value lines and lists of - item lines. Other front matter stops the import.

The import prints a report. Read problems, catalog_unknown, unclassified_decisions and kept_as_written.

You can import each domain one time. A second import of the same domain gets the refusal domain_exists. For a test, --fresh deletes the database first. --fresh only works on a SQLite file whose name contains test.

Then do a check of the import:

apex-memory verify ~/apex-notes/myproject

verify renders each page from the store and compares it with its file. It ignores spaces at line ends, extra blank lines and blank lines at the start and end. If a page is different, verify prints a diff and the exit code is 1.

Move a store from SQLite to Postgres

apex-memory import copies a full store when its argument is a file or a URL and not a folder.

  1. Create an empty Postgres database. See Postgres.

  2. Copy the store:

    apex-memory import ~/.apex-memory/memory.db --url postgresql://apex:secret@localhost:5432/apex_memory
  3. Do a check of each domain:

    apex-memory check myproject --url postgresql://apex:secret@localhost:5432/apex_memory
  4. Set APEX_MEMORY_URL to the Postgres URL in your shell profile and in each MCP client.

The copy keeps all rows, ids, the store id and the dates. It reads the source in one transaction and writes the target in one transaction. The target must not have notes or log entries. If it has, the copy stops with already holds knowledge. A copy of a store into the same store stops with cannot copy a store into itself.

The same command moves a store from Postgres to SQLite, or from SQLite to SQLite. The source can be a backup file or a URL.

Backups

apex-memory backup --to ~/.apex-memory/backups --keep 14

The command writes <folder>/memory-YYYY-MM-DD.db. Each backup is a SQLite file. The command does an integrity check of the file before it keeps it. It then deletes the oldest backups and keeps the newest --keep files. A second backup on the same day replaces the first one.

Backend

How the backup works

SQLite

VACUUM INTO a new file, then PRAGMA integrity_check.

Postgres

A logical copy of the store into a new SQLite file, then PRAGMA integrity_check. It does not need pg_dump.

For a full backup of a Postgres cluster, also use pg_dump.

To restore a backup, import it into a new empty store:

apex-memory import ~/.apex-memory/backups/memory-2026-10-30.db --url sqlite:///Users/you/.apex-memory/restored.db

The target of a restore can also be a Postgres URL.

Nightly backup on macOS

The file ops/com.apex-memory.backup.plist is a launchd agent. It runs apex-memory backup --keep 14 each day at 03:00. If the Mac is asleep at that time, the backup runs when the Mac wakes. The output goes to /tmp/apex-memory-backup.log.

The agent runs the command in a zsh login shell. Thus ~/.zprofile or ~/.zshenv must put ~/.local/bin on PATH. If you do not use the default database, the same file must set APEX_MEMORY_URL. A login shell that is not interactive does not read ~/.zshrc.

  1. Copy the file:

    cp ops/com.apex-memory.backup.plist ~/Library/LaunchAgents/
  2. Load the agent:

    launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.apex-memory.backup.plist
  3. To do a test now, run launchctl kickstart gui/$(id -u)/com.apex-memory.backup.

  4. Read /tmp/apex-memory-backup.log. Make sure that it shows the path of a new backup.

On Linux, use cron or a systemd timer to run the same command.

Differences between the backends

SQLite and Postgres give the same pages, checks, briefs, links and log queries. These differences remain:

Area

SQLite

Postgres

regex search

Python re syntax.

POSIX syntax of the ~ operator. The pattern must also compile in Python.

bm25 search

FTS5 bm25().

ts_rank with BM25 weights. The hits are almost the same, but the rank order can be different.

Tokens for bm25

Paths and URLs split into words.

Paths and URLs can stay as one token.

exact search

A trigram index for queries of 3 characters or more.

A scan of all section rows.

Single writer

BEGIN IMMEDIATE and WAL.

An advisory lock for each schema.

Backup

VACUUM INTO.

A logical copy into a SQLite file.

Search snippets

The match is in brackets, for example The [café] on.

Context around the match, with no brackets.

section_id values

A refused write uses no id.

A refused write can use an id. Thus ids can be different from the ids of the same history on SQLite. Do not compare ids between backends.

Regular expressions on Postgres

The store compiles each regex pattern in Python first. Then it sends the pattern to Postgres to find sections. It finds log entries with Python. Thus a pattern must have the same meaning in the two syntaxes.

  • Do not use \b. In Postgres, \b is a backspace, so the search finds no sections.

  • Do not use \y, \m or \M. Python refuses them with bad_regex.

  • For a word boundary, use character classes, for example (^|[^A-Za-z0-9_])copies([^A-Za-z0-9_]|$).

  • Do not use named groups (?P<name>...). Do not use inline flags after the start of the pattern.

Speed

exact search on Postgres scans rows. log_query with page scans rows on the two backends. This is fast for thousands of rows. For much larger stores, add an index.

Run the tests

  1. Clone the repository and go into it.

  2. Run the tests on SQLite:

    uv run python -m unittest

    The Postgres tests are skipped. The result is OK (skipped=47).

  3. To run the tests on Postgres too, install the extra:

    uv sync --extra postgres
  4. Start a Postgres server. The tests create and drop one schema for each test in the database that you give.

    docker run -d --name apex-memory-test-pg -e POSTGRES_PASSWORD=test -p 55432:5432 postgres:17
  5. Run the tests with the URL of that database:

    APEX_MEMORY_TEST_POSTGRES_URL=postgresql://postgres:test@localhost:55432/postgres uv run python -m unittest
  6. Remove the server: docker rm -f apex-memory-test-pg.

Add a database backend

The store reaches the database only through the Backend interface in apex_memory/backends/base.py. Each backend is one module.

  1. Write a module in apex_memory/backends/ with a subclass of Backend. Set name and location. location must not contain a password.

  2. Write the schema in apex_memory/backends/sql/<name>/001_schema.sql. migrate() applies the files in that folder in name order.

  3. Add one line for each URL scheme to BACKENDS in apex_memory/backends/__init__.py.

  4. Add a test class that runs MemoryTest on the new backend. Use PostgresMemoryTest in tests/test_memory.py as the example. Override new_url() to give a new empty store.

  5. Run all the tests.

The backend must supply these methods:

Method

What it must do

__init__(url)

Connect to the database that the URL names.

transaction()

Give one write transaction. Allow only one writer at a time across all processes. Commit on success. Roll back on an exception.

query(sql, args)

Run neutral SQL with ? placeholders. Return rows that you can index by position and by column name.

execute(sql, args)

Run one statement. Return the number of rows that changed.

insert(table, values)

Insert one row and return its integer id. Accept a given id and move the id sequence after it.

search(table, query, where, args, exact)

Do a ranked full-text search with score and snippet. When exact is true, do a substring search.

regex(expr)

Return a condition that is true when expr matches the next ? argument.

now(minutes)

Return SQL for the UTC time plus minutes as YYYY-MM-DDTHH:MM:SS.mmmZ.

schema_version(), set_schema_version(version)

Read and write the number of applied migrations.

run_script(sql)

Run one migration file.

backup(destination)

Write a full, verified copy of the store as a new SQLite file.

close()

Close the connection.

store.py, importer.py and server.py contain only neutral SQL: ? placeholders, lower, coalesce, count, max, LIKE ... ESCAPE '!', ||, LIMIT and EXISTS. All database-specific SQL goes in the backend. The test PortabilityTest.test_no_database_specific_sql fails if these files contain tokens such as fts5, PRAGMA, ON CONFLICT, RETURNING, ::, ILIKE or psycopg.

Agent rules template

templates/AGENTS.md is a template of rules for agents that use this store. Copy it into your own AGENTS.md or CLAUDE.md. Then replace the placeholders.

License

MIT. See LICENSE.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Markdown + SQLite knowledge store with bidirectional wikilinks, exposed as an MCP server for AI agents to maintain an interconnected knowledge base.
    11
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    MCP server for managing a local, domain-agnostic knowledge base using Markdown notes with frontmatter. Enables AI agents to capture, read, search, link, and maintain notes with atomic writes and privacy controls.
    13
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Provides AI agents with a local, private Markdown-based memory vault and SQLite search. Enables agents to search, read, list, and traverse linked knowledge pages via MCP with zero external runtime dependencies.
    7
    8
    MIT
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables creating, linking, searching, exploring, and synthesizing atomic notes as a knowledge graph through MCP-compatible clients such as Claude. Supports tags, note types, semantic relationships, Markdown storage, and an SQLite index for efficient traversal.
    -