<!-- title: 造记忆库MCP：跨会话存取笔记 -->

## 概述

本教程手把手带你用 TypeScript 从零造一个**带持久化能力的「记忆库 MCP 服务器」**。学完你将掌握：MCP 客户端/服务器/传输三件套的运行原理、如何用 `McpServer` 高层 API 声明带 schema 校验的工具、如何把数据落盘实现跨会话记忆、以及如何把它注册进 Claude Code 并用 MCP Inspector 调试。适合已跑通 Claude Code、想给自己的 AI 装上「长期记忆」的开发者。

## 前置条件

- 已安装并认证 **Claude Code**（CLI 可正常 `claude` 启动）
- **Node.js 18+**（本教程用 20 LTS；MCP stdio 服务器要求 Node 18 以上）
- 会基本的命令行操作，能看懂 TypeScript 与 `async/await`
- 一个空目录作为工作区，例如 `~/projects/memory-mcp`

---

## 一、先搞懂 MCP：客户端、传输、三原语

动手前先把概念理清，否则后面排错会两眼一抹黑。MCP（Model Context Protocol）本质是一套基于 **JSON-RPC 2.0** 的通信协议，让 LLM 客户端（Claude Code 就是其中一个客户端）能调用外部服务器暴露的能力。

三个核心概念：

1. **客户端（Host/Client）**：发起请求的一方。Claude Code 启动时会拉起你注册的服务器进程，并作为客户端与之对话。
2. **服务器（Server）**：你写的程序，暴露工具给客户端调用。
3. **传输（Transport）**：双方用什么通道收发消息。本地工具用 **stdio**（标准输入输出），远程服务用 **Streamable HTTP / SSE**。

服务器能暴露三类能力，也就是 MCP 的「三原语」：

- **Tools（工具）**：可被 LLM 主动调用的函数，比如「存一条记忆」「查天气」。这是本教程的主角。
- **Resources（资源）**：类文件的数据，客户端可读取，比如 API 返回结果、文件内容。
- **Prompts（提示词）**：预写好的模板，帮用户快速完成特定任务。

> ⚠️ **stdio 服务器的第一铁律**：**绝不能往 stdout 写日志**。stdio 通道上跑的就是 JSON-RPC 报文，你一旦 `console.log`，就会把协议报文污染掉，客户端立刻解析失败、连接断开。要打日志只能写 `console.error`（stderr）或落文件。

整体架构如下：

```mermaid
flowchart LR
    U[用户在 Claude Code 里提问] --> CC[Claude Code 客户端]
    CC -- "JSON-RPC over stdio" --> SVR["memory-mcp 服务器\n(Node 进程)"]
    SVR --> T1[remember 工具]
    SVR --> T2[recall 工具]
    SVR --> T3[forget 工具]
    SVR --> T4[list_memory 工具]
    SVR --> T5[search_memory 工具]
    T1 --> DISK[(store.json 磁盘)]
    T2 --> DISK
    T3 --> DISK
    T5 --> DISK
    DISK -. 跨会话持久化 .-> CC
```

## 二、初始化 TypeScript 项目

```bash
mkdir -p ~/projects/memory-mcp && cd ~/projects/memory-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init
```

这里用的是 **v1 稳定版** 的包名 `@modelcontextprotocol/sdk`——它在 2026 年仍是生产受支持版本。文末会提到 v2（`@modelcontextprotocol/server`）将于 2026-07-28 随 MCP 新规范稳定发布，到时 API 会从 `.tool()` 迁移到 `.registerTool()`。

**预期结果**：`package.json` 里出现依赖，目录多出 `node_modules` 与 `tsconfig.json`。

接着编辑 `package.json`，加上 `type` 与脚本：

```json
{
  "name": "memory-mcp",
  "version": "1.0.0",
  "type": "module",
  "bin": { "memory-mcp": "dist/index.js" },
  "scripts": {
    "build": "tsc",
    "dev": "tsx index.ts",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.x",
    "zod": "^3.x"
  }
}
```

调整 `tsconfig.json` 关键项（ESM 项目要 `NodeNext`）：

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "rootDir": ".",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["index.ts"]
}
```

**常见问题**：
- *报错 `ERR_UNKNOWN_FILE_EXTENSION .ts`*：忘了 `"type": "module"`，或没用 `tsx`/编译后产物运行。开发期用 `npm run dev`，正式注册用编译后的 `dist/index.js`。
- *`Cannot find module '@modelcontextprotocol/sdk/server/mcp.js'`*：确认 `moduleResolution` 是 `NodeNext`，且 SDK 版本在 1.x 以上。

## 三、搭建服务器骨架

新建 `index.ts`，先写持久化层与服务器实例：

```typescript
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";

// ---- 持久化层：记忆读写到本地 JSON ----
const STORE_PATH = process.env.MEMORY_STORE_PATH
  ?? path.join(os.homedir(), ".memory-mcp", "store.json");

type Item = { value: string; tags: string[]; updated: string };
type Store = Record<string, Item>;

function load(): Store {
  try {
    return JSON.parse(fs.readFileSync(STORE_PATH, "utf-8"));
  } catch {
    return {}; // 文件不存在或损坏，从空开始
  }
}

function save(data: Store) {
  fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });
  fs.writeFileSync(STORE_PATH, JSON.stringify(data, null, 2), "utf-8");
}

// ---- 创建 MCP 服务器 ----
const server = new McpServer({ name: "memory", version: "1.0.0" });
```

这段代码做了三件事：定义存储路径（可用环境变量覆盖，默认放 `~/.memory-mcp/store.json`）、提供 `load/save` 两个落盘函数、用 `McpServer` 高层 API 建一个名为 `memory` 的服务器。

## 四、设计并实现五个工具

下面用 `server.tool(名称, 描述, zod schema, 处理函数)` 声明五个工具。schema 同时承担「参数校验」和「给 LLM 看的入参说明」双重职责——描述写得越清楚，Claude 越知道何时该调它。

**工具 1：remember（存/更新一条记忆）**

```typescript
server.tool(
  "remember",
  "把一条事实或笔记存到长期记忆库，key 相同则覆盖。可带 tags 归类。",
  {
    key: z.string().describe("键名，如 user_prefers_tabs 或 api_base_url"),
    value: z.string().describe("记忆内容"),
    tags: z.array(z.string()).optional().describe("可选标签数组"),
  },
  async ({ key, value, tags }) => {
    const data = load();
    data[key] = { value, tags: tags ?? [], updated: new Date().toISOString() };
    save(data);
    return {
      content: [{ type: "text", text: `已记住 ${key}（库中共 ${Object.keys(data).length} 条）` }],
    };
  }
);
```

**工具 2：recall（按 key 取记忆）**

```typescript
server.tool(
  "recall",
  "按 key 取出一条记忆，不存在则提示未找到。",
  { key: z.string() },
  async ({ key }) => {
    const item = load()[key];
    if (!item) return { content: [{ type: "text", text: `没有找到 ${key} 这条记忆。` }] };
    return {
      content: [{
        type: "text",
        text: `${key} = ${item.value}\n标签: ${item.tags.join(", ") || "无"}\n更新于: ${item.updated}`,
      }],
    };
  }
);
```

**工具 3：forget（删除一条记忆）**

```typescript
server.tool(
  "forget",
  "按 key 删除一条记忆。",
  { key: z.string() },
  async ({ key }) => {
    const data = load();
    if (!(key in data)) return { content: [{ type: "text", text: `${key} 不存在，无需删除。` }] };
    delete data[key];
    save(data);
    return { content: [{ type: "text", text: `已忘记 ${key}。` }] };
  }
);
```

**工具 4：list_memory（列出全部，可按 tag 过滤）**

```typescript
server.tool(
  "list_memory",
  "列出记忆库中所有 key，可按 tag 过滤。",
  { tag: z.string().optional() },
  async ({ tag }) => {
    const data = load();
    const keys = Object.keys(data).filter(k => !tag || data[k].tags.includes(tag));
    if (keys.length === 0) return { content: [{ type: "text", text: "记忆库为空。" }] };
    return {
      content: [{
        type: "text",
        text: keys.map(k => `- ${k}：${data[k].value.slice(0, 40)}`).join("\n"),
      }],
    };
  }
);
```

**工具 5：search_memory（在 value 里全文检索）**

```typescript
server.tool(
  "search_memory",
  "在记忆内容中做大小写不敏感的全文检索，返回命中的 key 与内容。",
  { query: z.string() },
  async ({ query }) => {
    const q = query.toLowerCase();
    const hits = Object.entries(load()).filter(([, v]) => v.value.toLowerCase().includes(q));
    if (hits.length === 0) return { content: [{ type: "text", text: `没有命中 "${query}" 的记忆。` }] };
    return {
      content: [{ type: "text", text: hits.map(([k, v]) => `- ${k}：${v.value}`).join("\n") }],
    };
  }
);
```

最后**绑定 stdio 传输并启动**：

```typescript
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((err) => {
  console.error("memory MCP 启动失败：", err); // stderr，安全
  process.exit(1);
});
```

**编译验证**：

```bash
npm run build   # 产物在 dist/index.js
```

**预期结果**：编译无报错，`dist/index.js` 生成。直接 `node dist/index.js` 运行时，进程会**阻塞等待输入**（它在等 JSON-RPC 报文）——这是正常的，说明 stdio 服务器就绪了，按 `Ctrl+C` 退出即可。

**常见问题**：
- *工具返回值被 Claude 当成乱码*：检查返回结构必须是 `{ content: [{ type: "text", text: "..." }] }`，`type` 字段不能漏。
- *schema 报 `Input not match`*：zod 对象的 key 必须与解构参数名一致；可选参数用 `.optional()`。
- *日期/时间相关报错*：本服务器跑在 Node 进程里，`new Date()` 正常可用（别和某些脚本环境混淆）。

## 五、注册到 Claude Code

在**普通终端**（不要在 `claude` 会话里）执行注册命令：

```bash
claude mcp add memory -- node /绝对路径/dist/index.js
```

- `claude mcp add`：注册服务器
- `memory`：你起的名字，Claude 用它给工具打标签（工具会显示为 `mcp__memory__remember`）
- `--`：分隔符，其后是 Claude 要启动的命令
- 默认走 **stdio** 传输，无需 `--transport`

**想自定义存储位置**，加环境变量：

```bash
claude mcp add memory \
  --env MEMORY_STORE_PATH=/abs/path/store.json \
  -- node /abs/path/dist/index.js
```

**验证连接**：

```bash
claude mcp list
```

看到 `✓ Connected` 即成功。状态含义速查：`✓ Connected` 就绪；`! Connected · tools fetch failed` 服务器起来了但没列出工具；`✗ Failed to connect` 进程没起来。

**进会话使用**：

```bash
claude
# 然后输入：
用 remember 工具记住：本项目后端基地址是 https://api.example.com，标签打 backend
```

第一次调用会请求授权，批准即可。输出里工具调用会带 `memory` 前缀，确认走的是你的服务器。

**作用域（scope）很关键**，决定配置存哪、谁能用：

| scope | 存储位置 | 可见范围 |
|-------|---------|---------|
| `local`（默认） | `~/.claude.json` 当前项目条目下 | 仅你，仅当前项目 |
| `user` | `~/.claude.json` 顶层 `mcpServers` | 仅你，所有项目 |
| `project` | 项目根 `.mcp.json` | 克隆该仓库的所有人 |

记忆库这种全局能力，建议加 `--scope user`：

```bash
claude mcp add --scope user memory -- node /abs/path/dist/index.js
```

也可直接手写 **`.mcp.json`**（项目根，可提交到 git 作为「配置即代码」）：

```json
{
  "mcpServers": {
    "memory": {
      "command": "node",
      "args": ["/abs/path/dist/index.js"],
      "env": { "MEMORY_STORE_PATH": "/abs/path/store.json" }
    }
  }
}
```

改完 `.mcp.json` 要**重启会话**才生效（Claude Code 只在启动时读它）。

**常见问题**：
- *`/mcp` 显示 No MCP servers configured*：多半是在别的目录注册的。local 作用域绑定到注册时的目录；要么回到原目录，要么用 `--scope user`。
- *首次启动超时*：npx 首次下载包很慢，提高超时：`MCP_TIMEOUT=60000 claude`。
- *同名服务器已存在*：先 `claude mcp remove memory` 再加。

## 六、用 MCP Inspector 调试

服务器不连 Claude 也能单独测，用官方 Inspector：

```bash
npx @modelcontextprotocol/inspector node /abs/path/dist/index.js
```

它会启动一个本地 Web UI（默认 `http://localhost:6274`，具体端口看终端输出），浏览器打开后可以：

1. 看 **Tools** 列表，确认五个工具与入参 schema 都正确暴露
2. 手动填参数**调用** `remember`、`recall`，看返回
3. 检查 `store.json` 是否被正确写入

这是排查「Claude 里调不通」最快的办法——先在 Inspector 里证明服务器本身没问题，再回头查注册配置。

## 实战案例：让 Claude 跨会话记住你的项目偏好

一个完整可复现的链路。**第一次会话**：

```
你：记住，我们团队代码风格是用 4 空格缩进、禁止 any，标签打 convention
（Claude 调用 remember，key 自动取类似 convention_style）
你：再记住，部署用 pnpm deploy:prod，标签打 deploy
```

**关掉终端，喝杯咖啡，第二天开新会话**：

```
你：帮我写个新模块，按团队风格来
（Claude 调 list_memory 或 recall 取出 convention_style，照此生成代码）

你：怎么部署来着？
（Claude 调 search_memory "deploy" 命中并告诉你 pnpm deploy:prod）
```

这就是「长期记忆」的价值——`store.json` 在磁盘上，跨会话、跨项目（若用 user scope）都活着。你甚至可以让 Claude 在每次会话开头主动 `recall` 几个固定 key（如 `user_profile`、`project_context`）来快速「载入上下文」。

完整工作流：

```mermaid
sequenceDiagram
    participant U as 用户
    participant CC as Claude Code
    participant M as memory-mcp
    participant F as store.json
    Note over U,F: 第一次会话——写入
    U->>CC: 记住后端地址是 X
    CC->>M: tools/call remember(key,value)
    M->>F: 写入
    M-->>CC: 已记住
    Note over U,F: 新会话——读取
    U->>CC: 帮我连后端
    CC->>M: tools/call recall(api_base_url)
    M->>F: 读取
    M-->>CC: 返回 X
    CC-->>U: 用 X 发起请求
```

## 常见问题与排错

- **进程起来但 `tools fetch failed`**：八成是 handler 里 `throw` 了或返回结构不对。用 Inspector 跑一遍定位；检查所有 `return` 都是 `{ content: [...] }`。
- **stdout 被污染导致断连**：排查代码里有没有 `console.log`/`process.stdout.write`，全部改成 `console.error`。
- **zod 校验失败**：把 `.describe()` 写全，schema 要覆盖处理函数用到的每个参数。
- **路径相关坑**：注册命令里 `dist/index.js` 要用**绝对路径**，否则 Claude 在别的 cwd 启动会找不到文件。
- **想升级到 v2**：2026-07-28 后，包名换 `@modelcontextprotocol/server`，导入换成 `import { McpServer } from '@modelcontextprotocol/server'`、`import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'`，注册工具改用 `server.registerTool(name, { description, inputSchema }, handler)`，schema 引入 `zod/v4`。逻辑层（load/save/各 handler）完全不用动——这就是把业务与协议层解耦的好处。

## 延伸阅读

- 给记忆库加 **Resources** 原语：把 `store.json` 整体作为可读资源暴露，Claude 可用 `@memory` 引用
- 加 **Prompts** 原语：固化「每周回顾记忆库并清理过期条目」这类模板
- 把 stdio 换成 **Streamable HTTP** 上云，做成团队共享的远程 MCP
- 给敏感记忆加密落盘，或接入 SQLite 替代 JSON 以支撑更大规模与索引

## 训练营适用性评估

- **适合**：MCP 实战营、AI 工具链开发营、前后端工程师转 AI 工程的入门项目。本项目无外部 API 依赖、零成本可跑、成果立即可在日常工作中用上，复现成功率高。
- **教学时长**：约 **3–4 小时**（含环境准备 0.5h、讲解概念 0.5h、跟写代码 1.5h、注册调试 1h）。
- **难度**：中等偏入门。核心难点在理解 stdio 通信约束与 zod schema，代码量不大但概念密度高。
- **延伸作业**：让学员加一个 `tag` 管理工具，或把存储换成 SQLite，作为加分项。

## 参考来源

1. [Connect to MCP servers — Claude Code Docs](https://code.claude.com/docs/en/mcp-quickstart) — 官方注册、作用域、`.mcp.json`、排错权威说明
2. [Build an MCP server — Model Context Protocol](https://modelcontextprotocol.io/quickstart/server) — MCP 三原语、stdio 日志铁律、FastMCP 官方示例
3. [MCP TypeScript SDK — GitHub README](https://github.com/modelcontextprotocol/typescript-sdk) — v1/v2 包名、`McpServer` 高层 API、2026-07-28 规范路线图
4. [MCP Inspector — Debugging](https://modelcontextprotocol.io/docs/tools/inspector) — 官方调试工具用法
5. [Tools concept — Model Context Protocol](https://modelcontextprotocol.io/docs/concepts/tools) — 工具原语的设计原则与返回结构规范
方 MCP 介绍** — Redis 团队对"MCP 连接 AI 与数据"的产品视角阐述
- **2026 MCP 规范候选版** — `blog.modelcontextprotocol.io`，2026-07-28 发布的下一代规范（含 Streamable HTTP、无状态会话）
- **Claude Code `claude mcp` CLI** — `claude mcp add/list/get/remove` 管理服务器生命周期的官方命令

---

## 参考来源

1. [Introducing Model Context Protocol (MCP) for Redis — Redis 官方博客](https://redis.io/blog/introducing-model-context-protocol-mcp-for-redis/) — Redis 团队阐述为何用 MCP 连接 AI 与数据层
2. [MCP SDKs — Model Context Protocol 官方文档](https://modelcontextprotocol.io/docs/sdk) — TypeScript/Python SDK 的 `McpServer` 高层 API 与 Zod schema 集成
3. [Create MCP Server in Node.js: Step-by-Step Guide — ServerAvatar](https://serveravatar.com/create-mcp-server-nodejs/) — Node.js 从零搭建 MCP 服务器的分步教程，含项目结构
4. [The 2026-07-28 MCP Specification Release Candidate — MCP 官方博客](https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/) — 2026 年下一代 MCP 规范，涵盖 Streamable HTTP 传输与无状态会话模型
5. [Model Context Protocol (MCP) Architecture Guide: From N×M to N+M [2026] — Preporato](https://preporato.com/blog/model-context-protocol-mcp-architecture-guide-2026) — MCP 如何消除 N×M 集成税，2026 生产级 agent 栈架构解析

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
