A Model Context Protocol (MCP) server that gives AI agents memory that lasts. Built in Rust with rmcp for the protocol layer and SQLite for storage — no database server to run.
14 tools covering semantic recall, embedding disclosure, provenance-tracked writes, confirmation of inferred facts, temporal supersession, PII redaction, typed session state, replay snapshots, and resumable human-in-the-loop workflows.
cargo install --git https://github.com/zavora-ai/mcp-session-memory --tag v1.3.0The tagged GitHub install provides the trained local model in this release immediately. The
unqualified cargo install mcp-session-memory command may resolve to an older crates.io release.
This compiles and installs the binary to ~/.cargo/bin/mcp-session-memory.
Requires Rust 1.94.1 or newer. The default embedding model downloads once on first start and then runs locally from cache.
# Remember across restarts. Without MEMORY_DB, memory lasts only for this run.
export MEMORY_DB=~/.agent-memory.db
mcp-session-memoryAdd to claude_desktop_config.json:
{
"mcpServers": {
"session-memory": {
"command": "mcp-session-memory",
"env": { "MEMORY_DB": "/Users/you/.agent-memory.db" }
}
}
}Same shape, in .kiro/settings/mcp.json or .cursor/mcp.json:
{
"mcpServers": {
"session-memory": {
"command": "mcp-session-memory",
"env": { "MEMORY_DB": "${HOME}/.agent-memory.db" }
}
}
}- SQLite persistence included by default — set
MEMORY_DBto keep memories, sessions, events and snapshots in one file; without it the server explicitly uses run-local storage - Workflows survive restarts — a run paused for a human approval is still paused tomorrow
- Semantic recall — "how should invoicing be handled" finds "always send invoices as a PDF"
- Trained local embeddings — quantized all-MiniLM-L6-v2 runs in process and recovers synonymy; model files download once and are then reusable offline
- Runtime disclosure —
get_embedding_statusreports the active backend, dimensions, locality, synonym support, and fallback limitation - Provenance on every write —
told,observed,correctedorimported, so you can audit why an agent believes something - Confirmation gate — anything inferred is held as
proposedand cannot influence the agent until confirmed - Temporal supersession — a memory that stops being true is superseded and linked, not erased
- Automatic deduplication — the same fact told twice reinforces one memory instead of creating two
- Settledness ranking — repeatedly confirmed memories are recalled first
- Subject scoping — memory is partitioned by subject; one user's facts never reach another's
- Five memory types — turn, session state, profile, project, artifact
- Data classification — public, internal, PII, financial, health, legal-sensitive
- PII redaction —
redact_memoryscrubs sensitive spans in place - Replay snapshots — freeze session state for deterministic replay
- Resumable HITL — pause for approval, resume where you left off
- MCP 2025 + 2026 — stdio compatibility for both revisions, with Tasks, structured results, annotations, cache metadata, and sealed MRTR approvals
| Variable | Default | Purpose |
|---|---|---|
MEMORY_DB |
(none) | Path to the SQLite file holding memories, sessions, events and snapshots. Unset means everything lasts only for this run, the server warns you at startup, and a worked example is seeded so you can try the tools |
MCP_MEMORY_EMBEDDER |
local-model |
local-model uses quantized MiniLM. Set lexical only when a trained model cannot be used; the tool surface then discloses that synonyms are not reliable |
FASTEMBED_CACHE_DIR |
.fastembed_cache |
Model cache location; HF_HOME takes precedence |
HF_ENDPOINT |
Hugging Face | Optional model mirror for restricted networks |
MCP_REQUEST_STATE_KEY |
(none) | At least 32 high-entropy bytes, required for protected MCP 2026 writes |
RUST_LOG |
info |
Tracing filter, e.g. mcp_session_memory=debug |
Cargo features: sqlite and local-model are enabled by default. all-backends is retained as an alias for both supported components; this release does not claim unimplemented Redis or Postgres storage.
A request arrives over stdio and lands on one of 14 tools. Reads go through the recall path, which embeds the query and ranks stored memories by similarity. Writes go through the confirmation gate, which decides whether a memory acts immediately or waits to be agreed to. Both then reach the store traits, and whichever backend is configured behind them.
Everything durable lives in one file: memories with their vectors, session state, the event trail, and replay snapshots. One artefact to back up, move or delete, and no daemon to run.
src/
├── main.rs Entry point — transport, backend selection, MEMORY_DB
├── server.rs MCP tool router — 14 tools registered
├── lib.rs Crate root
├── error.rs Error types
├── types.rs Session, MemoryEntry, Provenance, MemoryStatus, DataClass
├── store.rs MemoryStore and SessionStore traits
├── semantic.rs Embedder trait, trained MiniLM default, explicit lexical fallback
├── memory_store.rs In-memory backend — this run only
├── sqlite_store.rs Memories and vectors in one file
└── sqlite_sessions.rs Sessions, events and snapshots in the same file
| Decision | File | Why |
|---|---|---|
| Recall by meaning | semantic.rs |
Quantized all-MiniLM-L6-v2 sentence vectors recover rewording and synonyms |
| Local by default | semantic.rs |
Model inference remains in process; only model files are downloaded on first use |
| Backend disclosure | server.rs |
get_embedding_status makes semantic capability and fallback limits visible to the agent |
| Vector migration | sqlite_store.rs |
Stored memories are re-embedded transactionally when the model identity changes |
| Confirmation gate | types.rs, server.rs |
Provenance::acts_immediately is the single place that decides |
| Supersession | types.rs |
MemoryStatus::Superseded plus superseded_by, so history stays answerable |
| One durable file | sqlite_store.rs, sqlite_sessions.rs |
A paused workflow and what the agent learned must survive together |
Three design choices are worth knowing about, because they change how you use the tools.
retrieve_memory embeds your query and ranks stored memories by cosine similarity, so a
question worded differently still finds the right fact.
The default embedder is the quantized sentence-transformers/all-MiniLM-L6-v2 model through
FastEmbed and ONNX. Model files download on first use, are cached locally, and inference never
sends memory text to a hosted embedding API. A real inference smoke test verifies that a query
using cheap ranks a memory using inexpensive above an unrelated sentence.
MCP_MEMORY_EMBEDDER=lexical selects the legacy hashed word/trigram implementation explicitly.
It is deterministic and needs no model file, but it cannot reliably recover synonyms. That
limitation appears in the retrieve_memory tool description and in get_embedding_status, not
only in this README.
When a query matches nothing above the active model's relevance floor, recall returns an empty
list. It never presents an unrelated memory as a semantic hit. Omit query when the intended
operation is to list what is known about the subject, most settled first.
Every write carries a provenance:
| Provenance | Meaning | Takes effect |
|---|---|---|
told |
The subject said it themselves | Immediately |
observed |
Inferred from what they did | After confirm_memory |
corrected |
Learned from a correction | After confirm_memory |
imported |
Came from another system | After confirm_memory |
Anything but told is stored as proposed, and proposed memories are never returned by
recall. So an agent cannot quietly acquire beliefs its user never gave it — and a stray
instruction inside a document cannot install itself as a permanent preference.
MEMORY_DB covers memories and session state, events and replay snapshots. That matters
most for human-in-the-loop work: a workflow paused for an approval is still paused after a
restart, at the state version it reached, with its typed state intact — which is what makes
resume_session mean anything.
It also means an operator has one artefact to back up, move, or delete.
When a fact changes, use supersede_memory rather than delete_memory. It writes the
replacement, marks the old entry superseded, and links the two. The old memory stops being
recalled but stays on the record, so an agent asked why did you do that? can still answer.
delete_memory is there for when erasure is genuinely what you want.
| Tool | Purpose | Risk Class |
|---|---|---|
get_session_state |
Read current typed session state | Read-only |
list_session_events |
Inspect operational events | Read-only |
retrieve_memory |
Recall scoped memory by meaning | Read-only |
get_embedding_status |
Disclose the active model and semantic limitations | Read-only |
store_memory |
Remember something, with its provenance | Internal write |
confirm_memory |
Agree to a memory that was inferred | Internal write |
supersede_memory |
Record that a memory is no longer true | Internal write |
update_memory |
Correct existing memory (new version) | Internal write |
delete_memory |
Remove sensitive/incorrect entries | Identity/Security |
redact_memory |
Redact PII from memory | Internal write |
list_memory_refs |
List memory that influenced a session | Read-only |
create_replay_snapshot |
Freeze session state for replay | Internal write |
resume_session |
Resume paused HITL workflow | External write |
terminate_session |
Stop session with audit reason | External write |
Prompt: "What's the current state of the refund session?"
{
"session_id": "ses_demo_001",
"agent_id": "support_agent",
"status": "paused",
"current_node": "manager_approval_gate",
"state_version": 3,
"workflow_state": {
"refund_amount": 89.99,
"eligibility": "approved",
"approval_required": true
},
"memory_refs": ["mem_demo_001"],
"data_classes": ["financial"]
}Prompt: "How does this customer like to be contacted?"
Tool call: retrieve_memory
{
"subject_type": "customer",
"subject_id": "cust_1187",
"query": "how does this customer like to be contacted"
}The stored memory says "prefers email follow-up" — no word in common with the question
except "customer", and it still comes back first. Omit query to get everything known
about the subject, most settled first.
Output:
[
{
"memory_id": "mem_demo_001",
"memory_type": "profile_memory",
"content": "Customer prefers email follow-up. Previous refund approved in March 2026.",
"data_class": "pii",
"confidence": 0.91,
"version": 1,
"status": "active"
}
]Prompt: "Remember that this customer's preferred language is Swahili"
{
"memory_type": "profile_memory",
"subject_type": "customer",
"subject_id": "cust_1187",
"content": "Preferred language: Swahili",
"data_class": "pii",
"provenance": "told",
"confidence": 0.95
}Output:
{ "memory_id": "mem_a1b2c3d4...", "status": "stored", "message": "Stored." }Told the same thing again, it reinforces rather than duplicating:
{
"memory_id": "mem_a1b2c3d4...",
"status": "already_known",
"reinforcements": 1,
"message": "A memory this close already exists; it has been reinforced rather than duplicated."
}Prompt: "They've shortened my summaries three times — remember they like them brief"
{
"memory_type": "profile_memory",
"subject_type": "user",
"subject_id": "u_42",
"content": "Prefers summaries under 150 words",
"provenance": "observed"
}Output:
{
"memory_id": "mem_9f8e7d6c...",
"status": "awaiting_confirmation",
"message": "Held until someone confirms it. It will not influence anything yet."
}Call confirm_memory with that id and it starts being recalled.
Prompt: "Payment terms moved from net 30 to net 60"
{
"memory_id": "mem_terms_001",
"content": "Net 60 terms for all customers",
"reason": "policy changed Q3"
}Output:
{
"superseded": "mem_terms_001",
"replaced_by": "mem_terms_002",
"reason": "policy changed Q3",
"status": "superseded"
}Prompt: "The manager approved the refund, resume the session"
{
"session_id": "ses_demo_001",
"resume_payload": { "approval_decision": "approved", "approver": "manager_jane" },
"reason": "Manager approved refund"
}Output:
{ "session_id": "ses_demo_001", "status": "active", "resumed": true }Prompt: "Freeze this session state for replay testing"
{ "session_id": "ses_demo_001" }Output:
{
"snapshot_id": "snap_f7a2b1c4...",
"session_id": "ses_demo_001",
"state_version": 3
}Prompt: "Stop this session — suspected policy violation"
{
"session_id": "ses_demo_001",
"reason": "Suspected policy violation during refund flow",
"reason_code": "policy_violation"
}Output:
{ "session_id": "ses_demo_001", "status": "terminated", "reason": "Suspected policy violation..." }| Type | Description | Mutability |
|---|---|---|
turn_memory |
Recent conversational context | Mutable during session |
session_state |
Typed workflow state | Versioned |
profile_memory |
User/customer facts across sessions | Policy-governed |
project_memory |
Workspace decisions and preferences | Policy-governed |
artifact_memory |
References to artifacts (not content) | References only |
git clone https://github.com/zavora-ai/mcp-session-memory
cd mcp-session-memory
cargo build --release
cargo test![]() James Karanja Maina |
|---|
Apache-2.0 — see LICENSE for details.
Part of the ADK-Rust Enterprise MCP server ecosystem.
Built with ❤️ by Zavora AI
This server implements the ADK MCP SDK contract:
- HealthCheck — async health probe for registry monitoring
- mcp-server.toml — manifest declaring tools, risk classes, and credentials
- Structured tracing —
RUST_LOGenv-filter for observability
This server is built with rmcp 3.1.2 and requires Rust 1.94.1 or newer. The rmcp 3 rollout retains legacy MCP initialization compatibility and targets MCP protocol revisions 2025-11-25 and 2026-07-28.
This server uses exact rmcp 3.1.2 and adk-mcp-sdk 0.3 with a minimum supported
Rust version of 1.94.1. It accepts stateless MCP 2026 requests with
per-request protocol, client identity, and capability metadata while retaining
the legacy MCP 2025-11-25 initialize flow for ordinary tools.
- Structured results: all 14 tools advertise output schemas and annotations; MCP 2026 clients receive parsed
structuredContent, and{ "ok": false }responses are marked as errors. - Tasks:
retrieve_memory,store_memory,supersede_memory,update_memory, andredact_memory, each with a 10-minute TTL and progress status. - MRTR approvals:
delete_memory,redact_memory,resume_session,terminate_session - Discovery and routing: rmcp serves on-demand discovery and validates the
per-request protocol envelope; HTTP deployments can route with
Mcp-MethodandMcp-Name. The packaged binary currently uses stdio. - Caching:
tools/listreturns a publicttlMsof 86,400,000 for MCP 2026; rmcp omits the cache fields for legacy clients. - Deprecated extensions: this server does not add new Roots, Sampling, or dynamic client-registration dependencies.
Protected tools require MCP_REQUEST_STATE_KEY with at least 32 high-entropy
bytes. All replicas must share that key so sealed approval state can resume on
another instance. Approval state is bound to the client identity, tool, and
arguments and expires after two minutes. Missing identity, invalid state,
rejection, or legacy protocol use fails closed. Task records are process-local
for the current stdio runtime; use a durable task store before deploying the
server behind scale-to-zero HTTP infrastructure.
