Every coding agent you've used so far — Claude Code, Cursor, Gemini CLI, OpenCode — is stateless between sessions by default. Close the terminal, reopen it tomorrow, and the agent has no idea it spent three hours yesterday debugging your Postgres connection pool, or that you decided against using Redis for session storage because your ops team vetoed adding another stateful service. Memory MCP closes that gap. It's a Model Context Protocol server that gives an agent a place to write things down and a way to look them back up, independent of the chat transcript and independent of any single IDE's session state.
The reference implementation is @modelcontextprotocol/server-memory, maintained under the official modelcontextprotocol/servers repository. It models memory as a knowledge graph — entities, observations attached to those entities, and typed relations between entities — rather than as a flat notes file or a vector store. That's a deliberate design choice, and it changes how you should think about what you store and how you query it back.
This topic covers what the server actually exposes as tools, how it persists data, why it beats re-explaining your codebase every session, and — just as important — what you should never put in there.
Core Memory MCP Tools: Store, Retrieve, Search, and Knowledge Graph Entities
The official memory server exposes nine tools over MCP. They map directly onto graph operations, not onto a generic "save note" / "load note" API:
create_entities— create one or more named nodes, each with anentityTypeand an initial list ofobservations(plain strings).create_relations— connect two existing entities with a typed, directional edge (e.g.service_adepends_onservice_b).add_observations— append new observation strings to an existing entity without touching its relations.delete_entities— remove entities and cascade-delete their relations.delete_observations— remove specific observation strings from an entity, leaving the entity and its relations intact.delete_relations— remove a specific typed edge between two entities.read_graph— dump the entire graph: every entity, every observation, every relation.search_nodes— substring/keyword match across entity names, entity types, and observation text.open_nodes— fetch specific named entities (and their directly connected relations) by name, without pulling the whole graph.
An entity looks like this once created:
{
"type": "entity",
"name": "payment-service",
"entityType": "microservice",
"observations": [
"Owns the /charges and /refunds endpoints",
"Migrated from Stripe API version 2020-08-27 to 2023-10-16 in March 2026",
"Requires idempotency keys on all POST requests, enforced in middleware/idempotency.go"
]
}
And a relation between two entities:
{
"type": "relation",
"from": "payment-service",
"to": "postgres-primary",
"relationType": "writes_to"
}
Relation types are your convention to define — the server doesn't validate them against a schema. Use active-voice verbs (depends_on, writes_to, owns, blocks) so read_graph output reads like a sentence when an agent reasons over it. A graph full of relations named rel1, rel2 is technically valid and practically useless.
The critical thing to internalize: search_nodes is not semantic search. There's no embedding model, no vector index, no fuzzy ranking. It's a case-sensitive-ish substring match against the JSON you stored. If you write "auth svc" in one observation and search for "authentication service" later, you get nothing back. Naming discipline substitutes for retrieval intelligence here — pick canonical entity names once and reuse them verbatim in every observation, relation, and future search query.
Tips
- Standardize entity names before your team starts writing memories —payment-service, not sometimespayment_service, sometimesPaymentSvc.
- Useopen_nodesinstead ofread_graphwhenever you know the entity names you need; it avoids loading the whole graph into context.
- TreatrelationTypeas a fixed vocabulary (depends_on,owns,blocks,supersedes) and document it somewhere your team can see it — a## Relation Typesnote inside the graph itself works.
Memory MCP Setup: Storage Backends, File Locations, and Scoping
Out of the box, @modelcontextprotocol/server-memory persists to a single file: newline-delimited JSON, one record per line, each tagged "type": "entity" or "type": "relation". There is no database, no server process holding state in memory between calls beyond what it re-reads from disk — every tool call reads the file, mutates in memory, and writes the file back out.
Default location is a memory.json relative to wherever the package resolves inside node_modules — which is exactly why you should never rely on the default. Override it explicitly with the MEMORY_FILE_PATH environment variable:
{
"mcpServers": {
"memory": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory"],
"env": {
"MEMORY_FILE_PATH": "/Users/dat/agent-memory/project-x.json"
}
}
}
}
Scoping decisions matter more than they look at first glance:
- Per-project file (
~/agent-memory/project-x.json) — the common default. One graph per repo, committed nowhere, lives on the developer's machine. - Per-repo, checked into the repo (
.agent-memory/graph.jsonunder version control) — makes memory shareable across a team, but every entity write becomes a git diff, and merge conflicts on a JSON-lines file are painful because two agents appending observations concurrently can interleave lines in a way that isn't valid JSON per-record anymore. - Global file shared across all projects (
~/.config/agent-memory/global.json) — tempting for "things I always want the agent to know" (your Git username, your preferred test framework), but it's the fastest way to pollute one project's context with another project's facts.search_nodesdoesn't namespace by project; a substring match against "database" will happily return observations from three unrelated repos.
The pragmatic middle ground most teams land on: one memory file per project, named after the repo, stored under a directory the agent's MCP config points to explicitly, never relying on relative-path defaults. If you run multiple agents (Claude Code and Cursor) against the same repo, point both configs at the same MEMORY_FILE_PATH — otherwise you'll build two divergent memories for one codebase and wonder why Cursor "doesn't know" something Claude Code learned yesterday.
mkdir -p ~/agent-memory
export MEMORY_FILE_PATH=~/agent-memory/english-grammar-platform.json
There's no built-in encryption, no access control, and no automatic backup. Treat the memory file like any other local secret-adjacent artifact — back it up if it took real effort to build, and keep it out of your repo's .gitignore-tracked history unless you deliberately want it versioned.
Tips
- Always setMEMORY_FILE_PATHexplicitly. Never trust the package default — it moves whenevernpxresolves a different cache path.
- One memory file per project, shared across every agent tool you use on that project, avoids fragmented, contradictory memories.
- Back up the memory file the same way you'd back up a local.env— it's not disposable once it has a few weeks of real observations in it.
Why Persistent Memory Beats Re-Explaining Context Every Session
The obvious pitch for memory MCP is "the agent remembers things." The less obvious but more important pitch is what it removes from your prompt discipline. Without persistent memory, every session that needs project-specific context starts with some version of a 200-word preamble: here's our stack, here's why we don't use ORM X, here's the naming convention for feature flags, here's the one gotcha in our CI pipeline. That preamble either lives in your head (and gets typed inconsistently every time) or lives in a static file like CLAUDE.md that the agent reads in full on every session regardless of relevance.
Memory MCP changes the retrieval shape from "read everything, every time" to "look up what's relevant, when relevant." A well-populated graph lets an agent do this mid-task:
Agent (internal reasoning, before touching the rate limiter code):
search_nodes("rate limit")
→ finds entity "rate-limiter" with observation:
"Uses token bucket, not sliding window — sliding window was
reverted in commit a3f21c9 because it caused burst rejections
under load-test traffic patterns from the mobile app"
That's a fact that took a real incident and a real revert to learn. Putting it in a static markdown file works too, until the file accumulates fifty such facts and the agent reads all fifty for a task that touches none of them, quietly eating context budget. Memory MCP's targeted lookup (search_nodes, open_nodes) is the actual advantage over static files — not that it "remembers," but that it remembers selectively.
There's a second, subtler benefit: a knowledge graph accumulates cross-references that a flat file structurally can't express well. "payment-service depends_on postgres-primary" and "postgres-primary has_constraint max-100-connections" combine into a fact neither statement states directly — the agent can infer that scaling payment-service's worker pool risks exhausting the connection limit, by walking two edges. A markdown bullet list doesn't compose like that; you'd need to write the inference out by hand every time.
The honest trade-off: this only pays off if someone actually writes the observations down, and writes them well. An agent doesn't automatically know that a revert was significant enough to remember — you (or a disciplined end-of-task prompt, covered in Module 17's real-world workflow topic) have to tell it to store that fact. Memory MCP eliminates re-explaining only for the things that got captured. It does nothing for the things nobody thought to record.
Tips
- The payoff is proportional to capture discipline — a sparse graph barely beats a goodCLAUDE.md; a well-tended one meaningfully beats it.
- Prefer recording why a decision was made over what the decision was — the "what" is usually visible in the code; the "why" is what gets lost.
- Multi-hop relations (A depends_on B, B has_constraint C) are where the knowledge-graph model earns its complexity over a flat notes file — use them.
What to Store and What to Never Store in Agent Memory
Store the things that are expensive to rediscover and cheap to state:
- Architectural decisions and their reasoning — "chose token bucket over sliding window because of load-test burst rejections."
- Non-obvious constraints — "postgres-primary caps at 100 connections on the current RDS tier."
- Naming and code conventions specific to this repo — "feature flags are prefixed
ff_, defined inconfig/flags.py, never inline." - Known-bad approaches already tried and rejected — this is the single highest-value category; it stops an agent from re-proposing a fix you already reverted.
- Ownership and ‘who/what to ask' — "auth-service changes require review from the security team, tagged
@sec-reviewin PRs." - Stable identifiers — API versions in active use, environment names, deployment targets.
Never store:
- Secrets, tokens, credentials, connection strings with passwords. The memory file is plaintext JSON on disk with no encryption. If an agent ever writes an API key into an observation because it saw it in a
.envfile during debugging, that key now lives forever in a file with no rotation reminder attached to it. - PII — customer names, emails, or any data subject-identifiable information that showed up in a bug report or log line during debugging.
- Anything that changes weekly. Current sprint status, "the bug we're fixing today," today's branch name — this decays before anyone reads it back and just adds noise to
search_nodesresults. Ephemeral state belongs in the conversation, not the graph. - Unverified agent speculation stated as fact. If the agent infers "this function is probably called from the billing cron job" without checking, and that gets stored as an observation, every future session inherits that guess as established truth. This is context poisoning, and it compounds — the next session builds another inference on top of the first bad one.
- Full file contents or large code excerpts. The graph is for facts about the code, not a second copy of the code. Point to file paths and line ranges instead; the codebase itself is the source of truth for content, and it doesn't go stale in the same way a duplicated snippet does.
{
"type": "entity",
"name": "billing-cron",
"entityType": "scheduled-job",
"observations": [
"GOOD: Runs nightly at 02:00 UTC via k8s CronJob defined in infra/cron/billing.yaml",
"BAD (never do this): DB password is postgres://admin:hunter2@prod-db:5432/billing"
]
}
That second line is illustrative of exactly what not to write — if you ever see an agent about to persist something that looks like a credential, stop it and redirect the observation to a description of where the credential is stored, not its value.
Tips
- Run a quick mental test before storing anything: "would I be comfortable if this observation appeared in a shared team wiki?" If not, don't store it.
- Prefer storing pointers (file paths, PR numbers, incident IDs) over pasting content — pointers stay accurate; pasted content goes stale.
- Periodically grep the memory file for suspicious patterns (password,token,secret,key=) — see the auditing workflow later in this module.
Tips
Tips
- Memory MCP's value is retrieval discipline, not just retention — usesearch_nodes/open_nodesfor targeted lookups instead of dumping the whole graph into every prompt.
- Point every agent tool you use against the same project at the sameMEMORY_FILE_PATH— divergent memories per tool defeat the purpose.
- Capture the "why" and the "already tried and rejected" categories first — they're the hardest facts to reconstruct from the code alone.
- Never store secrets, PII, or unverified speculation stated as fact — the file has no encryption and no fact-checking layer.