Skip to main content
Glama
Hilonew

second-brain-mcp

by Hilonew
README.md
# second-brain-mcp

把本地的 **Markdown 笔记库变成可检索、可对话写入的「第二大脑」**,
通过 [MCP](https://modelcontextprotocol.io)(Model Context Protocol)交给 Claude Code 等 AI 客户端按需调用。

你不再需要每次把笔记全量塞进 prompt——AI 会在需要时**自动检索最相关的几个片段**。
全程本地运行、本地索引,检索不调用任何外部 API,也不产生额外费用。

适合谁:手上有一堆 Markdown 笔记(Obsidian / Logseq / 纯文件夹都行),
想让 AI 真正「记得」它们,又不想把笔记上传到任何云端服务的人。

## 特性

- **增量建索引** — 只重新处理内容变过的文件(md5 判断),大库日常更新秒级完成
- **零操作的自动同步** — 服务在启动后与每次检索前按需补差,改完笔记直接提问就行,
  **不需要挂着任何后台进程**(陈旧判断只看目录 stat,代价极小)
- **索引离线建、在线只读** — BM25 倒排索引在维护阶段构建并落盘,服务端不常驻笔记原文,
  检索时内存里只有倒排表;索引过期靠「分片数 + 指纹」签名发现,能捕捉「数量没变但内容变了」
- **混合检索** — BM25 关键词路(中文走 jieba 分词)∥ 语义向量路,RRF 融合 + 余弦阈值过滤,兼顾「字面命中」和「语义相关」
- **语言自适应** — 中文(jieba 缺失时自动降级为字/双字 bigram)与英文知识库都开箱即用,中英混排也没问题
- **本地优先** — Embedding 本地跑(默认 `bge-small-zh-v1.5`,约 100MB、CPU 秒出、离线可用),笔记不出本机
- **隐私默认** — 不向 Chroma / HuggingFace 发送遥测,离线模式常开
- **对话反向写入** — AI 可以把新内容追加到指定笔记并立刻重建索引(可写名单可配置,只追加、不删除)
- **跨平台** — Windows / macOS / Linux,CPU / GPU 双依赖清单

## 工作原理

```
   Markdown 知识库 (kb_dir)
        │                       ▲
        │ ① 检索前自动补差        │ ③ 对话追加写入
        ▼                       │
   mcp_server.py ──MCP (stdio)──► Claude Code / 任意 MCP 客户端
        │
        │ ② 只读检索:BM25 关键词 ∥ 语义向量 → RRF 融合 → 余弦阈值过滤
        ▼
   ┌───────────────────────────────┐
   │ chroma_store   向量索引         │
   │ bm25_index     倒排索引(落盘)  │
   └───────────────────────────────┘
        ▲
        └── 首次建档:python ingest.py(要联网下 embedding 模型)
            可选预建:python watcher.py(仅当索引需要随时保持新鲜)
```

## 索引什么时候更新

这是本项目最容易被误解的一点,所以单独说清楚:**索引只在被读取的那一刻才有必要是新鲜的。**

因此同步发生在**读取侧**——`mcp_server.py` 在启动后、以及每次检索前各做一次增量补差:

1. 遍历目录,比对每个文件的 `mtime + size`(**不读文件内容**)
2. stat 没变 → 直接跳过
3. stat 变了才读文件算 md5;md5 没变(touch 过、编辑器原子保存)→ 只刷新 stat,不重新 embedding
4. md5 也变了 → 只重建这一个文件的分片

因为第 1 步只是一次目录遍历(几千个笔记通常是几十毫秒),它可以被频繁调用而**感觉不到**。
启动那次跑在后台线程里,等你开口提问时通常已经完成。

**所以你不需要挂任何后台进程,也不需要记得重跑脚本。** 改完笔记,直接提问。

`python ingest.py` 只在两种情况下需要手动跑:**首次建档**(要向 HuggingFace 下载模型,
而服务是离线模式运行的,下不了),以及想一次性补齐大量改动时。


## 快速开始(5 步,约 5 分钟)

> Python 3.10+;建议用独立虚拟环境(conda / venv),别污染 base。

```bash
# 1. 进入项目,装依赖(有 NVIDIA 显卡想 GPU 加速就换成 requirements-cuda.txt)
cd second-brain-mcp
pip install -r requirements.txt

# 2. 首次运行:自动生成 config.yaml(从 config.example.yaml 复制)
python ingest.py --kb-dir D:/my-notes

#    如果过了几秒提示 kb_dir 为空——先编辑 config.yaml,把 kb_dir 指到你的笔记目录:
#    # config.yaml
#    kb_dir: D:/my-notes
#    再跑一次 python ingest.py 就会建索引(首次自动下载模型约 100MB,之后离线)

# 3. 注册 MCP 服务到 Claude Code
#    Windows:  双击 register_mcp.bat(或用 register_mcp.bat "你的python.exe")
#    macOS/Linux: ./register_mcp.sh
#    上一步 ingest 跑过、模型已下载,注册才能避开离线问题

# 4. 验证注册
claude mcp list          # 应看到 second-brain(或你改的服务名)

# 5. (可选)自测检索,不启动 MCP 直接跑一遍完整流程
python mcp_server.py --search "第二大脑是什么"
```

然后正常启动 claude 聊天。问到笔记里已有内容时,AI 会自动调用
`search_my_knowledge_base`;说「帮我记一下……」时会调用 `update_knowledge_base` 追加写入。

**先跑 `ingest.py` 再注册,顺序很重要**:首次建档会下载 embedding 模型,
而 MCP 服务是固定离线模式运行的;不先下载,检索时模型加载会失败。

**之后就什么都不用管了。** 改完笔记直接提问即可——服务会在检索前自动把改动补进索引,
不用手动重跑脚本,也不用挂后台进程。原因见上面「索引什么时候更新」。

## 配置参考(config.yaml)

| 字段 | 默认 | 说明 |
| --- | --- | --- |
| `kb_dir` | 空(必填) | 知识库根目录,递归扫 `.md`,跳过隐藏目录 |
| `chroma_dir` / `hash_file` | 项目目录下 | 向量库存储 / 索引状态(每个文件的 md5 + mtime/size)。可删,重跑 ingest 即恢复 |
| `bm25_index_file` | 项目目录下 | BM25 倒排索引落盘位置。可删,下次检索自动重建 |
| `collection` | `personal_kb` | Chroma 集合名,换库实测时别忘换 |
| `embedding.model` | `BAAI/bge-small-zh-v1.5` | 换模型请同时删 `chroma_dir` 重建(维度可能变) |
| `embedding.device` | `auto` | `auto` / `cpu` / `cuda` |
| `chunking.chunk_size` / `chunk_overlap` | 400 / 50 | 切块字数与重叠 |
| `ignore.dirs` | 常见依赖/构建目录 | 命中目录名即整棵子树跳过,避免依赖包的 README 被当笔记索引 |
| `ignore.patterns` | `*.tmp.md` 等 | 文件名 glob,排掉编辑器的半成品写入。写 `[]` 关闭该类忽略 |
| `sync.on_startup` | `true` | 服务启动后在后台线程补一次索引差量,你提问时通常已就绪 |
| `sync.check_interval` | 60 | 两次陈旧性检查的最小间隔(秒)。设 `0` = 每次检索都检查 |
| `watch.debounce_seconds` | 2.0 | 仅 `watcher.py` 使用:监听事件合并窗口,窗口内多个事件折叠成一次重建 |
| `retrieval.top_k` | 5 | 最终返回片段数 |
| `retrieval.recall_k` | 25 | 每路候选数(RRF 交叉印证用) |
| `retrieval.min_cos_sim` | 0.4 | 向量路余弦阈值,宁缺毋滥 |
| `retrieval.bm25_k1 / bm25_b / rrf_k` | 1.5 / 0.75 / 60 | BM25 与 RRF 参数,一般不动 |
| `mcp.server_name` | `second-brain` | 注册进客户端时的服务名 |
| `mcp.allowed_write_files` | `[]` | 对话可追加写入的文件(相对 kb_dir),留空关闭 |

可用环境变量临时覆盖(`KB_DIR`、`KB_TOP_K`、`KB_EMBED_MODEL`、`KB_BM25_INDEX_FILE`、
`KB_SYNC_CHECK_INTERVAL`、`KB_SERVER_NAME` 等,
完整清单见 [kb_config.py](kb_config.py) 顶部注释),适合 CI 或临时换库。

## 日常维护

**日常不需要任何维护。** 改完笔记直接提问,服务会在检索前自动补差。

需要你出手的只有两种情况:**首次建档**(跑一次 `python ingest.py`,要向 HuggingFace
下载模型),以及**彻底重建**(改了 `chunk_size`、切块逻辑或 embedding 模型之后):

```bash
rm -rf chroma_store .file_hashes.json bm25_index.pkl.gz   # 全是衍生产物,删了不心疼
python ingest.py                                          # 全新重建
```

> **为什么改了切块参数必须彻底重建?** 索引状态只对「文件内容」敏感,认不出切块
> 参数变了——不重建的话库里会同时残留新旧两种切法的分片,而且不报错。

### 什么时候才需要 `watcher.py`

只有这两个场景:

- **不聊天的时候索引也必须是新的**,例如另一个程序直接读 Chroma 或倒排索引文件
- 知识库极大,想把同步开销彻底移出检索路径

它是个可选的加速器,不是必需品:

```bash
python watcher.py --verbose      # 看每个文件的处理明细
python watcher.py --debounce 5   # 批量同步大量文件时调大合并窗口
python watcher.py --poll         # 网络盘 / 虚拟共享目录上改用轮询
```

它和自动同步同时开着也不会损坏数据(索引状态是原子替换、分片按确定性 id 覆写),
最多是重复劳动。真想常驻的话,Windows 用任务计划程序,macOS/Linux 写 systemd / launchd 服务。

### 性能参考

自动同步的成本 ≈ 一次目录遍历 + 每个文件一次 stat。几百到几千个笔记是几十毫秒量级,
基本感觉不到。如果你有十万量级的笔记,把 `sync.check_interval` 调大(比如 600),
或者干脆改用 `watcher.py` 预建。

## 常见问题排查

| 症状 | 原因 / 处理 |
| --- | --- |
| 检索报「模型加载失败」 | 没先跑过 ingest,embedding 模型从未下载;先 `python ingest.py` 一次 |
| 首次 `ingest.py` 长时间无响应 | 模型需联网下载但连不上 HuggingFace(`WinError 10060`)。设镜像重试:Windows `set HF_ENDPOINT=https://hf-mirror.com`,macOS/Linux `export HF_ENDPOINT=https://hf-mirror.com`。**模型一旦缓存,之后全部离线加载,不再碰网络** |
| MCP 连上但工具不可用 | 环境变量 `HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE` 没传,服务启动时联网挂起(一键注册脚本已带) |
| 中文检索疑似失效 | 依赖缺 `jieba`:`pip install jieba`(会有一次性的降级警告)。降级走字/双字 bigram,能用但召回率明显下降 |
| 改完笔记搜不到 | ① 首次建档跑过 `python ingest.py` 吗?没跑过库里就是空的。② 文件是否被 `ignore` 规则排除(隐藏目录、`node_modules`、`*.tmp.md`)。③ 同步发生在检索时,先问一句触发它再确认 |
| 改了笔记,服务端好像没跟上 | 服务在每次检索前自动补差。刚改完就问可能撞上后台正在同步——再问一次即可。经常遇到可把 `sync.check_interval` 设为 `0`(每次检索都检查) |
| 要不要一直挂着后台进程 | **不用。** 同步发生在服务端检索前,不需要常驻进程,也不需要记得重跑脚本 |
| 一次保存触发了多次重建(仅 `watcher.py`) | 调大 `watch.debounce_seconds`。编辑器(尤其带自动保存/原子写入的)一次保存可能产生 create+move+modify 多个事件 |
| `watcher.py` 启动后一直没动静 | 正常。kb_dir 不存在时它会等目录出现(不退出);建好后会自动全量对账。也可能改动被忽略规则排掉了——加 `--verbose` 看明细 |
| `bm25_index.pkl.gz` 是什么,能删吗 | BM25 倒排索引,从知识库派生。**能删**,下次检索会自动重建(index 缺失时服务端会自己补上) |
| 索引疑似是旧的 / 结果对不上 | 彻底重建:删掉 `chroma_store`、`.file_hashes.json`、`bm25_index.pkl.gz` 后重跑 ingest |
| 想和已有的同名 MCP 服务并存 | 改 `config.yaml` 的 `mcp.server_name`(如 `my-brain`)再注册;同名服务无法并存 |
| `--search` 能通,注册进 claude 后用不了 | 确认 `config.yaml` 的 `kb_dir` 已配且**已 ingest 过** |
| 改了 `register_mcp.bat` 后报奇怪的「不是内部或外部命令」 | .bat 必须是**纯 ASCII**:cmd 按控制台代码页解析批处理,UTF-8 中文会让解析器错位、把半行注释当命令执行。改注释也用英文 |

## 依赖与 Python 版本

- 锁定版本见 [requirements.txt](requirements.txt)(CPU 可用)、[requirements-cuda.txt](requirements-cuda.txt)(GPU,需 NVIDIA 驱动 ≥ CUDA 12.6)
- 默认模型 `bge-small-zh-v1.5` 中英通用、只占 ~100MB 磁盘,embedding 计算纯本地

## 目录结构

```
second-brain-mcp/
├── README.md                 # 本文档
├── config.example.yaml       # 配置模板(含注释,可入仓库)
├── config.yaml               # 你的个人配置(.gitignore 已忽略,不入仓库)
│
│   ── 基础层:无副作用,所有入口共用 ──
├── kb_config.py              # 配置加载:默认值 ⇠ config.yaml ⇠ 环境变量
├── kb_scan.py                # 文件发现、忽略规则、安全路径解析(读写两侧共用同一条边界)
├── kb_text.py                # 切块与分词(入库与检索共用同一份实现)
│
│   ── 索引层 ──
├── bm25_index.py             # BM25 倒排索引:构建 / 落盘 / 加载 / 打分 / 过期签名
├── ingest.py                 # 增量引擎:sync_all(全库对账)+ apply_file_changes(按路径)
│
│   ── 入口层 ──
├── mcp_server.py             # MCP 服务:检索前自动补差 + 检索 + 对话写入(含 --search 自测)
├── watcher.py                # 【可选】常驻监听,改动后立刻重建;仅特殊场景才需要
├── register_mcp.bat          # Windows 一键注册
├── register_mcp.sh           # macOS / Linux 一键注册
├── requirements[-cuda].txt   # 依赖清单(CPU / GPU 两版)
├── LICENSE                   # MIT
└── examples/sample_kb/       # 演示知识库,跑通即懂
```

运行期产物(均已 gitignore):`chroma_store/`、`.file_hashes.json`(索引状态:每个文件的
md5 + mtime/size)、`bm25_index.pkl.gz`。

## 许可证

[MIT](LICENSE) —— 随意使用、修改、分发、商用,保留版权声明即可。