Claude Code Academy

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 key
  • recall — 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/node

tsconfig.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:

CategoryWhat to store
decisionArchitectural or design decisions with rationale
bugKnown bugs, root causes, workarounds
conventionTeam conventions (naming, patterns, file locations)
contextProject background, stakeholder constraints
personWho owns what, contact info

Extending This

  • SQLite backend — swap the JSON file for better-sqlite3 for faster queries on large memory stores
  • Vector search — embed memories and do semantic recall (find memories similar to a query)
  • Expiry — add a ttl field and prune stale entries automatically
  • Sync — write to a shared store (S3, KV) so the whole team shares memory