本教程手把手带你用 TypeScript 从零造一个带持久化能力的「记忆库 MCP 服务器」。学完你将掌握:MCP 客户端/服务器/传输三件套的运行原理、如何用 McpServer 高层 API 声明带 schema 校验的工具、如何把数据落盘实现跨会话记忆、以及如何把它注册进 Claude Code 并用 MCP Inspector 调试。适合已跑通 Claude Code、想给自己的 AI 装上「长期记忆」的开发者。
claude 启动)async/await~/projects/memory-mcp动手前先把概念理清,否则后面排错会两眼一抹黑。MCP(Model Context Protocol)本质是一套基于 JSON-RPC 2.0 的通信协议,让 LLM 客户端(Claude Code 就是其中一个客户端)能调用外部服务器暴露的能力。
三个核心概念:
服务器能暴露三类能力,也就是 MCP 的「三原语」:
⚠️ stdio 服务器的第一铁律:绝不能往 stdout 写日志。stdio 通道上跑的就是 JSON-RPC 报文,你一旦
console.log,就会把协议报文污染掉,客户端立刻解析失败、连接断开。要打日志只能写console.error(stderr)或落文件。
整体架构如下:
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
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 与脚本:
{
"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):
{
"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,先写持久化层与服务器实例:
#!/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(存/更新一条记忆)
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 取记忆)
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(删除一条记忆)
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 过滤)
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 里全文检索)
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 传输并启动:
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((err) => {
console.error("memory MCP 启动失败:", err); // stderr,安全
process.exit(1);
});
编译验证:
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 会话里)执行注册命令:
claude mcp add memory -- node /绝对路径/dist/index.js
claude mcp add:注册服务器memory:你起的名字,Claude 用它给工具打标签(工具会显示为 mcp__memory__remember)--:分隔符,其后是 Claude 要启动的命令--transport想自定义存储位置,加环境变量:
claude mcp add memory \
--env MEMORY_STORE_PATH=/abs/path/store.json \
-- node /abs/path/dist/index.js
验证连接:
claude mcp list
看到 ✓ Connected 即成功。状态含义速查:✓ Connected 就绪;! Connected · tools fetch failed 服务器起来了但没列出工具;✗ Failed to connect 进程没起来。
进会话使用:
claude
# 然后输入:
用 remember 工具记住:本项目后端基地址是 https://api.example.com,标签打 backend
第一次调用会请求授权,批准即可。输出里工具调用会带 memory 前缀,确认走的是你的服务器。
作用域(scope)很关键,决定配置存哪、谁能用:
| scope | 存储位置 | 可见范围 |
|---|---|---|
local(默认) |
~/.claude.json 当前项目条目下 |
仅你,仅当前项目 |
user |
~/.claude.json 顶层 mcpServers |
仅你,所有项目 |
project |
项目根 .mcp.json |
克隆该仓库的所有人 |
记忆库这种全局能力,建议加 --scope user:
claude mcp add --scope user memory -- node /abs/path/dist/index.js
也可直接手写 .mcp.json(项目根,可提交到 git 作为「配置即代码」):
{
"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 再加。
服务器不连 Claude 也能单独测,用官方 Inspector:
npx @modelcontextprotocol/inspector node /abs/path/dist/index.js
它会启动一个本地 Web UI(默认 http://localhost:6274,具体端口看终端输出),浏览器打开后可以:
remember、recall,看返回store.json 是否被正确写入这是排查「Claude 里调不通」最快的办法——先在 Inspector 里证明服务器本身没问题,再回头查注册配置。
一个完整可复现的链路。第一次会话:
你:记住,我们团队代码风格是用 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)来快速「载入上下文」。
完整工作流:
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: [...] }。console.log/process.stdout.write,全部改成 console.error。.describe() 写全,schema 要覆盖处理函数用到的每个参数。dist/index.js 要用绝对路径,否则 Claude 在别的 cwd 启动会找不到文件。@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)完全不用动——这就是把业务与协议层解耦的好处。store.json 整体作为可读资源暴露,Claude 可用 @memory 引用tag 管理工具,或把存储换成 SQLite,作为加分项。.mcp.json、排错权威说明McpServer 高层 API、2026-07-28 规范路线图blog.modelcontextprotocol.io,2026-07-28 发布的下一代规范(含 Streamable HTTP、无状态会话)
- Claude Code claude mcp CLI** — claude mcp add/list/get/remove 管理服务器生命周期的官方命令McpServer 高层 API 与 Zod schema 集成[!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 等场景全覆盖。
