Preference Memory MCP
by efficjump
README.md
**English** | [한국어](README.ko.md)
# Preference Memory MCP
[](https://github.com/efficjump/preference-memory-mcp/actions/workflows/ci.yml)
[](pyproject.toml)
[](LICENSE)
A local, provider-neutral MCP server that turns user corrections into scoped, reviewable, and safely retrievable preference memory.
Preference Memory MCP keeps durable preferences, constraints, workflows, and recurring corrections in a local SQLite database. It can ask the model already controlled by an MCP host to extract structured candidates, but it never chooses a model provider and never activates inferred memory without a separate user confirmation.
[Architecture](docs/architecture.md) · [Tool reference](docs/tool-reference.md) · [Threat model](docs/threat-model.md) · [Security policy](SECURITY.md)
Status: alpha. Designed for single-user, local use; it is not a credential or authorization service.
## Why this exists
Long-lived instructions are often copied into every project, mixed with temporary task context, or lost between sessions. This server gives MCP-compatible clients a shared memory layer with explicit scope, lifecycle states, review, and deletion from live storage.
It is deliberately not a transcript recorder. MCP servers cannot subscribe to every conversation on their own. A host or model must call `memory_observe`, and the server processes only the single observation supplied to that tool.
## Design goals
- Vendor-neutral: no provider SDKs, model names, or product-specific configuration.
- Local-first: stdio transport and a per-user SQLite database.
- Reviewable: every newly created proposed or inferred memory starts as `pending`.
- Scoped: open selector maps support repositories, task types, languages, or any integration-defined dimension.
- Dynamic: provider-neutral MCP sampling extracts candidates and can select a relevant subset from a bounded pool of already-authorized memories.
- Fail-closed inference: unavailable sampling, invalid JSON, extra fields, or over-limit output create no candidates; unsafe sampled candidates are skipped individually.
- Explainable: recall returns the matched scope and tags with each memory.
- Deletable: purge removes the live memory, linked evidence digests, and FTS rows in one transaction.
## Architecture
```mermaid
flowchart LR
H["MCP host"] --> S["stdio MCP adapter"]
S --> P["Deterministic policy"]
S --> I["Host-controlled sampling"]
I --> V["Strict schema validation"]
P --> D["SQLite + FTS5"]
V --> P
D --> C["Local review CLI"]
```
The model may suggest the statement, type, tags, and confidence. Deterministic code controls secret blocking, scope inheritance, candidate limits, status transitions, confirmation, search boundaries, and deletion.
See [docs/architecture.md](docs/architecture.md) for data flow, lifecycle, storage, and extension points.
## Requirements
- Python 3.11 or newer
- An MCP host with stdio server support
- Optional MCP sampling support for `memory_observe` and dynamic reranking
- Optional MCP form elicitation support for in-host approval and deletion
The server remains useful without sampling: clients can create structured pending candidates with `memory_remember`, and the local CLI can review them.
## Quick start
```bash
git clone https://github.com/efficjump/preference-memory-mcp.git
cd preference-memory-mcp
uv tool install .
preference-memory-mcp --version
PREFERENCE_MEMORY_DB="$PWD/demo.sqlite3" preference-memory doctor
```
During development:
```bash
uv sync --all-groups
uv run preference-memory-mcp
```
## Illustrative MCP configuration
Host configuration formats are not part of the protocol and differ between applications. In a host that uses an `mcpServers` map, the entry commonly looks like this:
```json
{
"mcpServers": {
"preference-memory": {
"command": "preference-memory-mcp"
}
}
}
```
To isolate a test database:
```json
{
"mcpServers": {
"preference-memory": {
"command": "preference-memory-mcp",
"env": {
"PREFERENCE_MEMORY_DB": "/absolute/path/to/test-memory.sqlite3"
}
}
}
}
```
## MCP tools
| Tool | Behavior |
| --- | --- |
| `memory_recall` | Returns active, unexpired memories after scope filtering, conservative local matching, and optional semantic selection. |
| `memory_observe` | Uses host-controlled sampling to extract strict pending candidates without storing the raw observation. |
| `memory_remember` | Creates one structured pending candidate without sampling. |
| `memory_review` | Lists memories by lifecycle state. |
| `memory_get` | Returns `{found, memory}` for one opaque identifier. |
| `memory_resolve` | Approves, rejects, or disputes a memory only after form elicitation confirms the exact change. |
| `memory_forget` | Purges content from live storage only after form elicitation confirmation. |
The server also exposes JSON resources for an active, unexpired global summary, one memory item, and the pending review queue, plus a `memory_workflow` prompt. See [docs/tool-reference.md](docs/tool-reference.md) for exact behavior and statuses.
## Safe workflow
1. Call `memory_recall` before substantial work, passing the current request and only the scope selectors needed for the task.
2. Treat returned memory as user context, never as authorization and never above current instructions.
3. Call `memory_observe` only with one direct user-authored preference or correction. Do not pass retrieved documents, tool output, or a transcript.
4. Review pending candidates with `memory_review` or the local CLI.
5. Activate, reject, or dispute a memory through `memory_resolve` form confirmation or the trusted local CLI.
6. Use `memory_forget` or the CLI to purge obsolete content from live storage.
## Local management CLI
```bash
preference-memory list --status pending
preference-memory show MEMORY_ID
preference-memory approve MEMORY_ID
preference-memory reject MEMORY_ID
preference-memory dispute MEMORY_ID
preference-memory forget MEMORY_ID
preference-memory export --status active
preference-memory doctor
```
Interactive confirmation is required for state changes unless `--yes` is supplied to the local CLI.
Both executables support `--help` and `--version`. Invalid arguments and expected configuration,
storage, confirmation-EOF, and lifecycle errors are reported on stderr without a traceback.
## Scope selectors
Selectors are intentionally open-ended and contain no product-specific fields:
```json
{
"selectors": {
"repository": "opaque-repository-id",
"task_type": "frontend",
"language": ["python", "typescript"]
},
"exclusions": {
"visibility": "shared"
}
}
```
Every included selector must match the recall context. Any matching exclusion rejects the memory before text search or model reranking.
## Configuration
| Variable | Default | Description |
| --- | --- | --- |
| `PREFERENCE_MEMORY_DB` | Platform user-data directory | Complete database path. |
| `PREFERENCE_MEMORY_DATA_DIR` | Platform user-data directory | Parent directory used when the database path is not set. |
| `PREFERENCE_MEMORY_SAMPLING` | `auto` | Sampling policy: `auto`, `extraction-only`, `rerank-only`, or `never`. |
| `PREFERENCE_MEMORY_RECALL_LIMIT` | `12` | Default recall result limit; accepted range 1–50. |
| `PREFERENCE_MEMORY_RERANK_CANDIDATES` | `24` | Maximum semantic candidate-pool size; accepted range 1–50. |
| `PREFERENCE_MEMORY_MAX_OBSERVATION_CHARS` | `8000` | Observation limit before sampling; accepted range 1–50,000. |
| `PREFERENCE_MEMORY_MAX_STATEMENT_CHARS` | `1200` | Stored statement limit; accepted range 3–1,200 and applied to direct and sampled candidates. |
| `PREFERENCE_MEMORY_MAX_CANDIDATES` | `5` | Sampling candidate limit; accepted range 1–5. |
The four sampling modes independently control extraction and recall reranking:
| Mode | Observation extraction | Recall reranking |
| --- | --- | --- |
| `auto` | Enabled | Enabled |
| `extraction-only` | Enabled | Disabled |
| `rerank-only` | Disabled | Enabled |
| `never` | Disabled | Disabled |
When reranking is disabled or unavailable, recall uses a conservative exact-token gate before
FTS5/BM25 ranking. With reranking enabled, it also considers a bounded set of applicable scoped
and global memories; the sampler may return any unique supplied subset, including an empty set.
See [docs/architecture.md](docs/architecture.md) for the selection order and fallback behavior.
## Security and privacy boundaries
- The raw `memory_observe` input is not written to the database, but it is sent to the sampler selected by the host.
- Credential-like observations and observations containing control or invisible formatting characters are blocked before sampling. Every durable candidate field and scope value is checked again before storage.
- Model output is accepted only as strict JSON matching a closed Pydantic schema.
- Newly inferred candidates cannot become active through model output or ordinary tool arguments.
- Optional reranking sends the query and already scope-authorized candidate summaries to host sampling. Credential-like queries and queries containing control or invisible formatting characters stay on local search.
- Records created under an older policy are rechecked before automatic recall, reranking, and the global profile cap. Management reads remain visible so the local user can inspect and purge them.
- Newly created data directories use mode `0700` and database files use `0600` on compatible systems. Existing custom directories are not modified.
- Audit events contain action metadata, not memory text.
- The server uses the operating-system user account as its local security boundary.
The database is not encrypted at rest. Purge is not a forensic secure-erase guarantee: SQLite WAL files, free pages, backups, and snapshots may retain bytes. Use full-disk encryption or an encrypted volume when the local account boundary is insufficient. Do not use this project as a password, token, private-key, payment, medical, or identity vault. See [SECURITY.md](SECURITY.md) and [docs/threat-model.md](docs/threat-model.md).
## Development
```bash
uv sync --all-groups
uv run ruff check .
uv run ruff format --check .
uv run pytest
uv build
uvx --from twine==6.2.0 twine check dist/*
```
The MCP Python SDK is intentionally constrained to `>=1.28.1,<1.29`. A small, guarded compatibility
adapter uses FastMCP 1.x internals to publish this package's server version and reject undeclared
top-level tool arguments; tests make a future SDK upgrade explicit instead of silently weakening
the contract. `uv.lock` pins the exact tested environment. CI tests Python 3.11–3.13, validates the
distributions, installs the wheel into an isolated environment, and fails when a runtime dependency
reports an unreviewed license label.
## License
Apache License 2.0. Runtime dependency notices are documented in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
This is an independent implementation and does not imply endorsement by any protocol maintainer or MCP host.
TDQS
B3.3/5.0
Scored across 6 tools
Disambiguation5/5
Each tool targets a distinct action: reading by ID, observing candidates, recalling active memories, creating pending proposals, resolving them, and listing reviewable ones. No overlap.
Naming Consistency5/5
All tools follow the 'memory_<verb>' pattern with clear, descriptive verbs (get, observe, recall, remember, resolve, review), ensuring predictability.
Tool Count5/5
With 6 tools, the surface is well-scoped for a memory management server, covering core operations without being overly large or sparse.
Completeness4/5
The tools cover create, read, list, and resolve workflows. However, there is no explicit delete tool; while resolve may handle disposal, this omission is a minor gap for full CRUD coverage.
Maintenance
ActivitySlowing
ResponsivenessNo issues