Build a Session Memory MCP Server
By default, Claude Code starts fresh every session — it has no memory of what you discussed yesterday. This tutorial builds an MCP server that gives Claude persistent memory: a structured store it can read from and write to across conversations.
The Problem
Claude's context window is cleared when you close a session. If you:
- Made a key architectural decision last week
- Investigated a bug and found the root cause
- Set up a convention your team agreed on
...Claude won't know any of this next time you open it.
The solution: a lightweight MCP server backed by a local JSON file (or SQLite) that Claude can query at the start of a session and update as it learns things.
Architecture
Claude Code session
│
▼
memory-mcp (MCP server)
│
▼
~/.claude/memory/
└── project-slug.json ← per-project memory store
Claude gets two tools:
remember— store a fact with a category and keyrecall— retrieve facts by category or key
Step 1 — Project Setup
mkdir memory-mcp && cd memory-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/nodetsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"strict": true
},
"include": ["src"]
}Step 2 — Memory Store
Create src/store.ts:
import fs from 'fs'
import path from 'path'
import os from 'os'
export type MemoryEntry = {
key: string
category: string
value: string
updatedAt: string
}
type Store = Record<string, MemoryEntry>
function storePath(project: string): string {
const dir = path.join(os.homedir(), '.claude', 'memory')
fs.mkdirSync(dir, { recursive: true })
return path.join(dir, `${project}.json`)
}
function load(project: string): Store {
const p = storePath(project)
if (!fs.existsSync(p)) return {}
return JSON.parse(fs.readFileSync(p, 'utf-8'))
}
function save(project: string, store: Store): void {
fs.writeFileSync(storePath(project), JSON.stringify(store, null, 2))
}
export function remember(project: string, category: string, key: string, value: string): void {
const store = load(project)
store[`${category}:${key}`] = { key, category, value, updatedAt: new Date().toISOString() }
save(project, store)
}
export function recall(project: string, category?: string): MemoryEntry[] {
const store = load(project)
const entries = Object.values(store)
if (!category) return entries
return entries.filter(e => e.category === category)
}
export function forget(project: string, category: string, key: string): void {
const store = load(project)
delete store[`${category}:${key}`]
save(project, store)
}Step 3 — MCP Server
Create src/index.ts:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
import { remember, recall, forget } from './store.js'
const server = new McpServer({
name: 'memory-mcp',
version: '1.0.0',
})
// Store a memory
server.tool(
'remember',
'Store a fact, decision, or piece of context for later recall',
{
project: z.string().describe('Project identifier (e.g. "payments-api")'),
category: z.string().describe('Category: decision | bug | convention | context | person'),
key: z.string().describe('Short unique key (e.g. "auth-strategy")'),
value: z.string().describe('The fact or decision to remember'),
},
async ({ project, category, key, value }) => {
remember(project, category, key, value)
return {
content: [{ type: 'text', text: `Remembered [${category}] ${key}` }],
}
}
)
// Recall memories
server.tool(
'recall',
'Retrieve stored memories for a project, optionally filtered by category',
{
project: z.string().describe('Project identifier'),
category: z.string().optional().describe('Filter by category — omit to get all'),
},
async ({ project, category }) => {
const entries = recall(project, category)
if (entries.length === 0) {
return {
content: [{ type: 'text', text: 'No memories found.' }],
}
}
const formatted = entries
.map(e => `[${e.category}] ${e.key}: ${e.value}\n (updated ${e.updatedAt})`)
.join('\n\n')
return {
content: [{ type: 'text', text: formatted }],
}
}
)
// Forget a memory
server.tool(
'forget',
'Remove a stored memory',
{
project: z.string(),
category: z.string(),
key: z.string(),
},
async ({ project, category, key }) => {
forget(project, category, key)
return {
content: [{ type: 'text', text: `Forgot [${category}] ${key}` }],
}
}
)
async function main() {
const transport = new StdioServerTransport()
await server.connect(transport)
console.error('memory-mcp running')
}
main().catch(console.error)Step 4 — Build and Connect
npm run build # compiles to dist/Add to ~/.claude/mcp_servers.json (global — works across all projects):
{
"mcpServers": {
"memory": {
"command": "node",
"args": ["/absolute/path/to/memory-mcp/dist/index.js"]
}
}
}Step 5 — Use It in CLAUDE.md
Tell Claude to use the memory server at the start of each session. Add to your project's CLAUDE.md:
## Session Memory
At the start of each session, call `recall` with project="payments-api" to load
any stored context before answering questions or making changes.
When you learn something non-obvious (architectural decision, root cause of a bug,
team convention), call `remember` to store it for future sessions.Example Workflow
Session 1 — you investigate a bug:
You: find out why the settlement job fails on the last day of the month
Claude: [investigates, finds it's a timezone issue in Carbon::endOfMonth()]
Claude: [calls remember(project="payments-api", category="bug", key="settlement-eom", value="Carbon::endOfMonth() returns UTC midnight — use ->setTimezone('Asia/Kuala_Lumpur') before comparing")]
Session 2 — a colleague asks about it:
You: do we know anything about the settlement job?
Claude: [calls recall(project="payments-api", category="bug")]
Claude: Yes — there's a known timezone issue: Carbon::endOfMonth() returns UTC midnight...
Claude remembered — without you having to re-explain.
Memory Categories
Use consistent categories so recall is predictable:
| Category | What to store |
|---|---|
decision | Architectural or design decisions with rationale |
bug | Known bugs, root causes, workarounds |
convention | Team conventions (naming, patterns, file locations) |
context | Project background, stakeholder constraints |
person | Who owns what, contact info |
Extending This
- SQLite backend — swap the JSON file for
better-sqlite3for faster queries on large memory stores - Vector search — embed memories and do semantic recall (find memories similar to a query)
- Expiry — add a
ttlfield and prune stale entries automatically - Sync — write to a shared store (S3, KV) so the whole team shares memory