Skip to main content
Glama
wcsmars

Personal Memory MCP

by wcsmars

Personal Memory MCP

tests

A local memory service for assistants that answers a question most memory stores cannot: what did the system know at a given time? Every fact is an immutable, dated assertion. Retrieval at a historical cutoff returns only what was known then, ranked as if no other version of the corpus existed.

The point-in-time discipline is the same one a backtest needs. A store that only keeps the latest version, or that searches one global index and filters dates afterwards, lets information from outside the cutoff into past answers. This project measures that leak and removes it.

  • SQLite storage with append-only revisions, two time axes, tombstones and source provenance

  • Hybrid retrieval: SQLite FTS5 (BM25) plus exact cosine scoring, combined with reciprocal rank fusion

  • Optional sqlite-vec native vector scoring; the pure-Python fallback produces the same rankings

  • A dependency-free MCP stdio server, plus an adapter for the official MCP Python SDK v2

  • Bounded, atomic Markdown import; JSON CLI; no model or network calls; fictional sample data only

Results: leakage in common shortcuts

benchmarks/temporal_leakage.py builds a seeded synthetic corpus: 400 memories plus a second namespace, 982 revisions dated 2024-01-01 to 2025-11-03, including later revisions, bounded corrections of past periods and deletions. It runs 299 queries at random historical cutoffs (42 of them ask about an earlier period than the cutoff).

Ground truth is computed independently of the store: a brute-force Python implementation of the visibility rule below, then BM25 from a fresh FTS5 index over exactly the visible rows. A query counts as affected when its top 5 contains different memories from the ground truth.

Retrieval approach

Top 5 differs

Detail

Latest version only (an overwriting store)

95.7%

59.9% of queries return a revision not yet known at the cutoff

One global index, top 20 matches, then filter

39.5%

30.1% lose results entirely: invisible rows fill the candidate list

Same, top 50 / top 100 matches

20.1% / 16.1%

7.4% / 3.0% lose results

Same, every match kept, then filter

13.0%

No truncation: BM25 statistics still count invisible rows

Index of rows known by the cutoff, then filter

9.4%

No future rows, but superseded and deleted versions still distort BM25

This store: resolve visibility, then index and rank

0.0%

Same memories, same order and identical BM25 scores on every query

Future rows are not the only problem. Even with no future rows indexed, superseded and deleted versions shift corpus statistics enough to change almost one query in ten.

PYTHONPATH=src python3.11 benchmarks/temporal_leakage.py

Full output, including order-only differences, is in benchmarks/results/temporal_leakage.json. The corpus is synthetic and small, so the rates show the mechanism, not a production estimate. Tests rerun a smaller version, check it is deterministic, and check the saved output is current.

Related MCP server: agent-fact-system

Quick start

Python 3.11 or newer. The core has no third-party dependencies.

./run-demo.sh

The demo stores two versions of one fictional preference. The same memory ID returns "equity research" as of February 2025 and "quantitative trading" as of April 2025. The script uses .venv/bin/python when present, otherwise python3.11 (override with MEMORY_PYTHON). Without the script:

PYTHONPATH=src python3.11 -m personal_memory --db local/demo.db --vector-backend python demo

Installed, with the native vector backend and the official SDK:

python3.11 -m venv .venv
.venv/bin/python -m pip install -e '.[all]'
.venv/bin/memory --db local/demo.db demo
.venv/bin/python -m pytest -q

Data model: two times, immutable assertions

Field

Meaning

known_at

When this assertion became available to the memory system

valid_from, valid_to

Half-open interval during which the assertion applies

memory_id, namespace

Identity of one fact and its retrieval scope

source, content_hash

Source reference and SHA-256 of the stored content

A query takes a knowledge cutoff as_of and, optionally, a valid_at time (default: the cutoff). It selects assertions known by the cutoff whose validity interval contains valid_at; for each identity the newest of those wins.

WITH eligible AS (
  SELECT *, ROW_NUMBER() OVER (
    PARTITION BY namespace, memory_id ORDER BY known_at DESC, id DESC) AS rn
  FROM revisions
  WHERE namespace = :namespace AND known_at <= :as_of
    AND valid_from <= :valid_at AND (valid_to IS NULL OR :valid_at < valid_to)
)
SELECT * FROM eligible WHERE rn = 1 AND deleted = 0;

This is an interval overlay: a bounded correction replaces earlier values only inside its interval, and an earlier open-ended assertion shows through outside it. A permanent change uses an open-ended assertion; removing a claim for a period uses an explicit tombstone. It is not a full bitemporal database with automatic interval splitting.

Triggers reject UPDATE and DELETE on the revisions table. forget appends a tombstone that records which revision it hides, so a query before the deletion still sees the old fact. Repeating an identical forget is a no-op, and forgetting a memory that is already hidden at that time is an error. Forget is not secure erasure; there is no purge operation.

All times are normalized to UTC with microsecond precision. Accepted forms are YYYY-MM-DD (meaning 00:00:00 UTC) and ISO 8601 timestamps with a timezone; anything else, including out-of-range or sub-microsecond values, is rejected with a message rather than silently adjusted.

Retrieval pipeline

  1. Apply namespace, known_at and valid-time filters and resolve versions and tombstones (the SQL above).

  2. Copy only those rows into a temporary FTS5 table and rank with BM25. Rows outside the cutoff cannot change corpus statistics. Indexed text and queries are Unicode-normalized (NFKC), and query words are passed to FTS5 without case folding, so forms such as "Straße", "finance" or decomposed accents match their stored text.

  3. Score the same rows by cosine similarity, using sqlite-vec vec_distance_cosine when installed, otherwise Python.

  4. Rank each component with tied scores sharing a rank, fuse with sum(1 / (60 + rank)), order by fused score then memory ID, and return each result with component ranks, raw scores, provenance and content hash.

sqlite-vec computes in 32-bit floats and Python in 64-bit, so equal cosine scores can differ by about 1e-7 between them. Distances within 1e-6 of each other are therefore treated as ties, and BM25 scores within a relative 1e-9 likewise, because operating-system maths libraries can differ in the last bit. With that rule, the two backends returned identical rankings on 10,000 randomized searches; a test checks this on every run.

The built-in embedding is a deterministic, signed 256-dimensional hash of words and adjacent word pairs. It is a reproducible lexical baseline, not a semantic embedding model, and works offline. The database records the embedding version and refuses to open with a different one. Vector search is an exact scan over visible rows, not an approximate index, which suits personal-scale collections rather than millions of rows.

Architecture

src/personal_memory/
  store.py      SQLite schema, append-only writes, visibility query, hybrid search, export
  embedding.py  Deterministic hashed word/bigram vectors and cosine distance
  markdown.py   Bounded, atomic Markdown import with stable chunk IDs
  cli.py        JSON command-line interface
  stdio.py      Dependency-free MCP stdio server (tools subset) and shared server settings
  server.py     Launcher; --sdk selects the official MCP SDK v2 adapter
benchmarks/     Leakage benchmark and saved results
tests/          Temporal, ranking, import, CLI, stdio protocol and SDK round-trip tests

More detail on the schema, invariants and operating limits: docs/architecture.md.

CLI

memory() { PYTHONPATH=src python3.11 -m personal_memory --db local/memory.db "$@"; }
memory import examples/notes --namespace demo
memory search "statistics practice" --namespace demo
memory remember preferred-role "I prefer equity research." --namespace personal --title "Career preference"
memory history preferred-role --namespace personal
memory export local/snapshot.json --namespace personal

Add --as-of 2025-02-01 to search, history or export for a historical cutoff, and --valid-at to ask about an earlier period using what was known at the cutoff. Read-only commands (search, history, export, stats) open the database read-only and report a missing file instead of creating one; init, import and remember create it. Errors are printed as JSON on stderr with exit code 2, and export never overwrites an existing file.

Markdown import. Recurses through visible .md files, skips symlinks, rejects NUL bytes and invalid UTF-8 (naming the file), and caps files at 2 MB. Line endings are normalized and a leading byte-order mark is dropped. Text is split at blank lines into chunks of at most 1,800 characters; a longer paragraph is cut at the last whitespace before the limit. Only whitespace between paragraphs is changed. The title is the first ATX heading (# to ######) outside fenced code, or the file name, capped at 200 characters.

Chunk IDs derive from collection, relative path and chunk number. Re-importing unchanged chunks is a no-op, changes append revisions, and a shrunk document tombstones its surplus chunks. Missing or renamed files are not deleted automatically, and re-importing a file restores a chunk that was forgotten while the file still contains it. Each import is validated in full before one transaction commits it, and re-import time grows linearly with the number of files.

known_at defaults to the actual import time; file modification dates are never treated as evidence of when something was known. --known-at on the CLI is a trusted assertion for historical datasets that the store cannot verify. MCP writes always use the server clock.

MCP server

PYTHONPATH=src MEMORY_DB="$PWD/local/memory.db" MEMORY_NAMESPACE=personal \
  python3.11 -m personal_memory.server          # add --sdk for the official SDK adapter

examples/mcp-config.json is a client configuration template; replace the placeholder paths. Use an absolute MEMORY_DB: a relative one resolves against the client's working directory. Each server process is pinned to one namespace.

Tool

Behavior

memory_search

Search visible memories, optionally at historical dates

memory_history

Audit revisions known by a cutoff, including tombstones

memory_status

Namespace revision count, vector backend, write availability

memory_remember

Append an assertion (only with MEMORY_ENABLE_WRITES=true)

memory_forget

Append a current-time tombstone (only with writes enabled)

Without MEMORY_ENABLE_WRITES=true the server registers only the three read tools and opens the database read-only, so it works on a write-protected file and reports a mistyped path instead of creating an empty database. Invalid configuration, such as an unknown vector backend or sqlite-vec requested but not installed, stops the server at startup with a one-line message.

The built-in server negotiates protocol 2025-11-25 (and echoes 2025-06-18 to clients that request it). It implements initialization, ping, tools/list and tools/call over newline-delimited JSON. It has a 4 MiB message limit, rejects duplicate JSON keys, validates tool arguments strictly, and ignores unknown request fields and client responses. It does not implement HTTP transport, resources, prompts, tasks or cancellation.

Both adapters share tool descriptions, annotations (read-only, destructive and idempotent hints), argument bounds and the untrusted-data instructions. Both return structured content, and both report bad dates or missing memories as tool errors with the reason, so the model can correct itself. Two SDK behaviors differ: the SDK adapter ignores unknown arguments (the namespace and knowledge time stay pinned either way), and it reports an unknown tool name as a tool error rather than a protocol error. There is no import tool and no SQL tool. A local stdio server is not a hosted HTTPS connector; whether a given assistant client can use it depends on that client.

Testing

.venv/bin/python -m pytest -q

86 tests cover:

  • temporal behavior: revisions, retroactive corrections, validity boundaries, deletion history, namespace isolation, and BM25 and rank invariance when future documents are added;

  • edge cases: time parsing, Unicode search and tie handling;

  • backend parity between sqlite-vec and Python;

  • Markdown import: bounds, headings, chunking, idempotence and atomic failure;

  • the CLI and read-only access;

  • the stdio server as a real subprocess: lifecycle, version negotiation, malformed input, size limits and read-only default;

  • the official MCP client against both adapters;

  • the benchmark.

With the extras installed all 86 pass; without them the 5 tests that need mcp or sqlite-vec are skipped. GitHub Actions runs the full suite on Python 3.11 and 3.12. See VALIDATION.md.

Limits

  • No inference: there is no model-based extraction, entity resolution or merging of contradictory notes.

  • Text is kept as written: remember stores text exactly as supplied; imports change only whitespace between paragraphs and line endings.

  • Chinese and Japanese text: keyword search uses the FTS5 unicode61 tokenizer, which treats an unspaced run of CJK characters as one word, so a partial phrase matches only through the vector component, if at all.

  • Security: there is no encryption (protect the SQLite file with OS permissions and disk encryption) and no secure erasure.

  • Deployment: there is no hosted service and no multi-process coordination beyond SQLite WAL.

  • MCP writes and the clock: MCP writes assume the server clock does not step backwards.

  • Outside knowledge: memory correctness alone cannot stop a consuming model from using knowledge it acquired elsewhere.

References

License

MIT. See LICENSE.

Available Tools

3 tools
memory_historyA
Read-only

Audit revisions known by cutoff, including tombstones. Historical values may conflict; this is not answer context.

NameRequiredDescriptionDefault
as_ofNo
memory_idYes

TDQS

A3.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly and closed-world behavior; the description adds real behavioral context by disclosing tombstone inclusion and that historical values may conflict. It still omits return shape and ordering, but the data-quality caveats are valuable beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two compact sentences, with the core purpose first and the caveat second. No filler; every clause adds scoping or warning information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool is simple and annotated read-only, and the description gives enough to understand the audit purpose and caveats. But with no output schema and no parameter descriptions, the agent still lacks return shape and precise input semantics, so it is only partially complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must explain both parameters. 'Known by cutoff' loosely maps to as_of, but memory_id is never described and no cutoff/date format or semantics are given.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description identifies a specific resource—audit revisions of a memory—and scope—known by a cutoff, including tombstones. It does not explicitly name memory_search or memory_status or use a direct retrieve/list verb, so sibling differentiation is only implicit.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It signals an audit/historical use case and explicitly warns 'this is not answer context,' which is a useful when-not cue. However, it never names the sibling tools to use for answer context or states exactly when audit history is required over memory_search, leaving alternatives implied.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

memory_statusA
Read-only

Report configured namespace, its revision count, vector capability, and write availability.

NameRequiredDescriptionDefault

No parameters

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and openWorldHint=false, so the safety profile is covered by structured data. The description adds what the response contains (namespace, revision count, vector capability, write availability), which is genuinely useful, but says nothing about auth requirements, cost, or how 'write availability' is determined.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence that enumerates exactly what is returned with zero filler or repetition of the tool name.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no parameters and no output schema, the description carries the burden of describing the return payload and does so by listing the four reported values. Only 'write availability' remains vaguely defined, a minor gap for a zero-arg read-only status tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so there is nothing for the description to disambiguate; baseline is 4. The description correctly does not waste words on parameter talk.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a specific verb-plus-content set: it reports the configured namespace, revision count, vector capability, and write availability. That is a clear, concrete purpose distinguishable in spirit from memory_search and memory_history, though it never names the siblings to make the contrast explicit.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is only implied: the enumerated status fields suggest a pre-flight/diagnostic check before searching or writing. There is no explicit 'use when' statement, no mention of when not to use it, and no routing to memory_search or memory_history.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 3 tool updatesv0.2.0
    • First observedmemory_history
    • First observedmemory_search
    • First observedmemory_status

TDQS

B3.4/5.0

Scored across 3 tools

Disambiguation4/5

Each tool has a distinct role: search retrieves answer content, history audits revisions/tombstones, and status reports configuration. Search and history both perform temporal memory lookups, so an agent could occasionally confuse them, but the descriptions explicitly demarcate answer context versus audit data.

Naming Consistency5/5

All three tools use the same memory_ prefix with a short noun-like suffix, giving a clear and predictable pattern. No mixing of conventions or verb styles.

Tool Count4/5

Three tools is on the low side but each maps to a genuinely different capability (retrieve, audit, diagnose). The thinness is acceptable for a focused read-oriented memory interface, though it borders on under-scoped.

Completeness2/5

The surface is entirely read-only: there is no tool to store, update, or delete a memory, so the core write lifecycle of a personal memory system is absent. The mention of 'write availability' in status hints at writes happening elsewhere, which agents cannot invoke through this server.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Provides a local stdio MCP server that gives AI coding agents a typed, temporal, evidence-backed memory on embedded SQLite, letting them record decisions and constraints, ask why something happened, navigate memory by time, trace proof paths between refs, and export or supersede stored state without rewriting it. It also exposes read-only ChronoLoom view tools so a person and their agent can steer the same multidimensional memory view by semantic intent rather than raw coordinates.
    Apache 2.0