内容
<p align="center">
<img src="assets/logo.svg" alt="mnemodoc" width="160" height="160">
</p>
<h1 align="center">mnemodoc-server</h1>
[](https://github.com/mnemodoc/mcp-server/actions/workflows/ci.yml)
[](LICENSE)
[](https://github.com/mnemodoc/mcp-server/releases)
一款索引项目文档的Crystal MCP服务器,通过Ollama嵌入式搜索提供混合搜索(语义搜索 + 关键词搜索)功能,以便MCP客户端使用。
## 为什么
每次Claude Code会话加载全部文档上下文都很昂贵且不可靠:大量的上下文信息会被压缩掉,而且代理只会检索它已经知道要查找的内容。mnemodoc-server允许Claude按需获取相关段落(降低令牌成本),并且在每次编辑前自动注入正确的约定,以便无论代理是否询问,指导都会到位。
## 一个思考方式——图书管理员
**在MnemoDoc之前**,每次会议都开始于将七个厚厚的活页夹放在桌上“以防万一”。桌子被压垮了,没有人能找到任何东西,而且仅仅是坐下就很昂贵。(`/context-reload`)
**MnemoDoc是图书管理员**。它从不给你整个图书馆。你问一个问题,它会返回**三个回答你问题的页面**——并告诉你这些页面来自哪些书。(`query_documents` + 可追踪的块)
为了做到这一点,它做了两件事:
- 它**将每本书分成连贯的页面**,而不是随机的复印件——一页=一个完整的想法。(章节感知的分块)
- 它从两个方面了解其书架:**按含义**(“它讲的是什么”)**和按确切的词语**在页面上。当你提问时,它会交叉检查两者,以免拿错书架。(语义 + 关键词 + RRF)
它也有一个条件反射:**最近修订的内容,它更倾向于放在堆的顶部**——因为在文档中,新鲜的内容往往更重要。(最近更新权重)
它的诚实基于一个原则:**它在书籍更改时立即检查书架**,所以它从不带着一副坦率的态度引用过时的页面。(`mtime`爬虫)
**还有读者呢?**(AI)图书管理员可能会将正确的页面放在桌子上,但仍需要有人阅读它们。读者带着**一副眼镜**:“以律师的身份阅读”,“以密码学家的身份阅读”。(角色选择器)
眼镜不会添加**任何书籍**——既不添加到图书管理员的书架上,也不添加到读者的记忆中。它们会改变**阅读的清晰度**:有了合适眼镜,读者可以将页面与他已经知道的内容联系起来,识别出外行人会跳过的细节,并用正确的语言写报告。但是,如果给专家眼镜却**没有东西放在桌子上**,读者不会说“我没有什么可读的”:他们会写一份漂亮的专家报告……关于他们想象的页面。(角色幻觉)
**道德**。我们没有让助手“成为您项目的专家”——而是给它配备了**合适的图书管理员**。眼镜**使阅读更清晰**;只有图书管理员**提供**桌上的内容。而且总是图书管理员先说话:没有眼镜的桌子只会产生海市蜃楼。🌅😊
<details>
<summary>🇫🇷 法语版本</summary>
## 特性
- **机械注入的上下文角色** — 角色选择引擎暴露为MCP工具**和**CLI,因此`PreToolUse`钩子可以保证在每次编辑前,正确的约定都能传递给代理,而不仅仅是当它记得询问时。([详情](#contextual-roles--the-pretooluse-hook))
- **多格式索引** — 轻量级标记语言,HTML/XML文档词汇,Office和OpenDocument文件,电子书和笔记本——所有这些都使用纯stdlib,无需外部工具——另外可选PDF,由每个格式的处理程序注册表调度。([完整列表](#supported-formats))
- **章节感知块分割** — 在文档的标题边界处分割每个文档(例如Markdown中的`##`/`###`),而不是任意的令牌计数。
- **混合搜索** — 语义(Ollama嵌入,vec0 KNN索引)+ 关键词(SQLite FTS5 / BM25)与RRF融合。
- **可插拔的向量后端** — 默认情况下,语义KNN在嵌入式vec0索引上运行,或者选择**Qdrant**(`search.backend: qdrant`)以获得远程/可扩展的存储;SQLite始终是事实的来源,而Qdrant是一个尽力而为的可重建索引。
- **本地和私有** — 通过Ollama(本地或Docker)进行嵌入,无需外部数据发送。
- **两个传输** — stdio(Claude Code)和HTTP(Cursor,其他MCP客户端)
- **静态二进制文件** — 单个可执行文件,无运行时依赖项
## 支持的格式
文件按扩展名分派给处理程序。下面所有文件都**开箱即用,无需外部依赖**(纯Crystal stdlib),除了PDF。
| 系列 | 扩展名 |
|---|---|
| Markdown / MDX | `.md` `.markdown` `.mdx` `.mkd` `.mdown` `.mdwn` `.markdn` `.mdtext` `.mmd` `.qmd` (Quarto)`.rmd` (R Markdown)|
| Org-mode | `.org` |
| AsciiDoc | `.adoc` `.asciidoc` |
| reStructuredText | `.rst` |
| HTML / XHTML | `.html` `.htm` `.xhtml` |
| DocBook | `.dbk` `.docbook` |
| DITA | `.dita` (仅主题;`.ditamap` 是引用,不是正文)|
| Jupyter 笔记本 | `.ipynb` |
| 纯文本 | `.txt` `.text` (+ 未知文件名的显式`paths`)|
| Word | `.docx` `.docm` `.dotx` `.dotm` |
| PowerPoint | `.pptx` `.pptm` `.potx` `.potm` `.ppsx` `.ppsm` |
| LibreOffice Writer | `.odt` `.ott` `.fodt` |
| LibreOffice Impress | `.odp` `.otp` `.fodp` |
| EPUB | `.epub` |
| FictionBook | `.fb2` |
| **PDF** (可选) | `.pdf` — 需要`pdftotext`在`PATH`中;启用`index.pdf: true` |
每个处理程序都读取**和**解析文件为章节感知的块;处理程序从不因损坏或格式错误的文件而引发错误(它们记录警告并跳过),因此一个错误的文件永远不会中止索引运行。
### 未索引
故意超出范围。这些需要真正的解析器或外部工具,或者对文档搜索的价值不大:
- **电子表格** — `.xlsx` / `.ods` / `.xls` (表格数据,非正文)。
- **传统二进制办公软件** — `.doc` / `.ppt` / `.xls` (OLE)和`.rtf`;需要外部转换器(`antiword`,LibreOffice…)如同PDF需要`pdftotext`。
- **LaTeX / TeX / Texinfo** — `.tex` `.latex` `.texi` (重标记;需要真正的剥离器)。
- **其他轻量级标记语言** — Textile,MediaWiki / `.wiki`,Creole,Gemtext,man/roff(小众;每个都有一个小型专用解析器)。
- **专有/二进制** — Apple iWork(`.pages` `.key` `.numbers`),Kindle(`.mobi` `.azw*`),WordPerfect(`.wpd`),Visio(`.vsdx`),OneNote(`.one`),DjVu,漫画(`.cbz`/`.cbr`)。
- **非文档文件** — 源代码,配置/数据(`.json` `.yaml` `.toml` `.csv`),电子邮件,字幕,日历,订阅源,图像。mnemodoc索引**文档正文**,而非代码或结构化数据。
## 快速入门
**1. 启动Ollama**
```sh
docker run -d --name ollama -p 11434:11434 ollama/ollama
docker exec ollama ollama pull nomic-embed-text
```
**2. 安装mnemodoc-server**
```sh
# macOS
brew install mnemodoc/tap/mnemodoc-server
# Linux — 从发行页面下载适用于您架构的二进制文件:
# https://github.com/mnemodoc/mcp-server/releases
```
**3. 在您的项目中创建配置文件**
```sh
# 下载示例配置文件
curl -fsSL https://raw.githubusercontent.com/mnemodoc/mcp-server/master/.mnemodoc.example.yml \
-o .mnemodoc.yml
# 然后编辑.mnemodoc.yml以设置您的文档路径
```
**4. 索引您的文档并测试** (可选 — `serve` 在启动时自动索引)
```sh
mnemodoc-server index doc/ --config .mnemodoc.yml
mnemodoc-server search "how to persist a model" --config .mnemodoc.yml
```
**5. 添加到您的MCP客户端**
*Claude Code* (`~/.claude/settings.json`) — stdio传输,无需网络暴露:
```json
{
"mcpServers": {
"doc": {
"command": "mnemodoc-server",
"args": ["serve", "--config", "/path/to/project/.mnemodoc.yml"]
}
}
}
```
*Cursor* (`.cursor/mcp.json`)— HTTP传输,首先启动服务器:
```sh
mnemodoc-server serve --sse --config /path/to/project/.mnemodoc.yml
```
```json
{
"mcpServers": {
"doc": {
"url": "http://localhost:8765/mcp"
}
}
}
```
## CLI
```sh
mnemodoc-server serve [--config .mnemodoc.yml] # Claude Code (stdio,默认)
mnemodoc-server serve --sse [--port 8765] [--host 127.0.0.1] # Cursor / 其他客户端
mnemodoc-server index <path> # 索引文件或目录
mnemodoc-server search "<query>" [--mode hybrid|semantic|keyword] [--top 5] # 从终端测试搜索
mnemodoc-server status # 索引统计
mnemodoc-server delete <path> # 从索引中删除
mnemodoc-server context [--files <path>]... [--task <kind>] [--query "<text>"] # 解析并打印要采用的角色
mnemodoc-server info # 版本信息
mnemodoc-server prompt-hook # 客户端钩子:在提示时注入最佳段落(stdin)
mnemodoc-server daemon status # 该项目的守护进程是否正在运行?
mnemodoc-server daemon stop [--timeout 10] # 优雅地停止它
```
### 在代理询问之前注入文档
`query_documents`仅在代理决定调用它时触发。`prompt-hook`消除了这种依赖关系:它与`UserPromptSubmit`绑定,对用户的消息进行索引搜索,并注入单个最佳段落——无论代理是否认为需要查找。
```json
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command",
"command": "mnemodoc-server prompt-hook --config /path/to/.mnemodoc.yml" }] }
]
}
```
**它保持沉默,除非提示可测量地关注此语料库。** 门槛是最佳段落与`hook.similarity_threshold`(默认`0.515`)的余弦相似度——不是搜索分数,这是一个看起来相同的排名神器,对于脱离主题的查询和有针对性的查询来说是一样的。下面阈值时,对于不可解析的输入,Ollama关闭时,在空索引或缺少配置时:它打印空并退出0。钩子在用户面前出错比说空话更糟糕。
**它注入的段落数量根据排名的把握程度而适应。** 当亚军在`hook.margin_threshold`(默认`0.02`)之内时,顺序不确定,因此候选人一起查看 — 最多`hook.max_passages`(默认`3`)。 决定性的领先发送一个。
该规则是衡量的,而不是猜测的。始终注入一个段落是正确的83.3%的时间,在基准语料库上始终注入三个是94.4%。 加宽仅在细微差别上是相同的94.4%,而在18个提示中6个提示上加宽 — **1.67个段落和平均108个令牌**,而语料库有1411个令牌。 即使有决定性和不确定的情况重叠,因为加宽一个已经正确的成本会带来令牌和准确性从来没有。
默认阈值是衡量的,而不是选择的:在基准语料库上18个项目问题得分0.542以上,而8个脱离主题的提示峰值达到0.488。那个0.054的幅度很薄,所以把0.5作为一个有防御性的起点,并为自己的语料库重新校准 — `mise bench:tokens`报告在实际问题上的触发率 **和** 脱离主题提示的假触发率,这是仅凭实际问题从未看到的校准阈值。
### 机器可读输出
每个返回结果的子命令都接受`--json` — `index`,`search`,`status`,`delete`,`context`,`info`,`daemon status`,`daemon stop`. `serve`和`prompt-hook`不这样做:一个流协议,另一个为客户端钩子提供段落以供直接使用,发出结果为stdout上的单个JSON对象:
```sh
$ mnemodoc-server status --json
{"version":"1.0.0 (c47d61c)","db_path":"/path/.mnemodoc/index.db","files":12,"chunks":86,"ollama":{"host":"http://localhost:11434","model":"nomic-embed-text"}$
$ mnemodoc-server search "retry policy" --json | jq -r '.results[0].content'
```
`search --json`携带表格省略的块主体,在相同的键名下与`query_documents`MCP工具(`file`,`heading`,`parent_heading`,`content`,`score`),所以两个接口使用相同的词汇。
**错误**在`--json`下以`{"error": "..."}`形式发出 **stderr**,stdout留空,退出代码1 — stdout仅携带结果,因此解析它不能吞下失败。
**负载增加**:字段可能会添加,从不删除或重命名。没有版本协商进行协商。
`--quiet`打印所有内容并通过退出代码报告。 可用的地方是`index`,`delete`,`daemon status`,`daemon stop`.
两个命令在结果而不是崩溃时退出非零。`daemon status`在没有守护进程运行时退出1,就像`systemctl is-active`一样。`index`在索引失败时退出1,大多数情况下是不可访问的Ollama。 运行结果为0,即使没有任何操作完成,而JSON负载带有`failed`计数器,无论如何。 `--quiet`退出代码是整个报告,这就是为什么该情况不能被读取为成功。
## 行为笔记
**每个项目的守护进程,自动生成代理** - 默认情况下(`server.daemon: true`),`serve --stdio` 不直接为 MCP 提供服务。它作为一个薄的代理,连接到每个项目的后台守护进程,该守护进程拥有 SQLite 索引。在第一次连接时,代理自动生成守护进程并等待它(最多 30 秒)。来自任何客户端(Zed、Claude Code、并行代理会话)的后续 `serve --stdio` 会话都连接到同一个守护进程;只有一个进程曾经触摸过索引,消除了并发写入和重复索引竞争。守护进程在空闲 `server.daemon_idle_timeout` 秒后自动退出(默认 600 秒 / 10 分钟),并在下一个请求时重新生成。套接字、锁定和 pid 文件位于索引 DB 旁边(`daemon.sock`、`daemon.lock`、`daemon.pid`);`daemon status` 和 `daemon stop` 从 CLI 驱动守护进程。不需要客户端配置更改 - 客户端仍然像以前一样启动 `serve --stdio`。如果守护进程在会话中间死亡,代理会自我修复(在文件锁定下重新生成,≤ 3 次尝试);如果尝试失败,它会退回到进程内独立处理程序,用于该会话的其余部分 - 不会重新索引,只会提供现有的磁盘索引。设置 `server.daemon: false` 以退出并恢复到独立的 stdio 服务器。
**实时重新索引(守护进程)** - 当守护进程运行时,它会监视配置的 `paths` 并在文档创建、修改或删除后约 1 秒内重新索引文档(通过 `file_watcher` 分片轮询)。默认启用(`server.daemon_watch: true`);使用 `server.daemon_watch_interval` (秒)调整节奏或设置 `daemon_watch: false` 以仅保留启动时间索引。只有守护进程监视;独立的 stdio 备用不监视。
**启动时的自动索引** - `serve` 在后台自动重新索引配置文件中的所有 `paths`。服务器立即响应;索引并发发生。文件自上次运行以来未更改的 mtime 将被跳过,因此重启很便宜。
### 提示钩子
`prompt-hook` 读取标准输入上的客户端钩子有效载荷,并在 `UserPromptSubmit` 上注入最佳匹配段落,以便文档到达代理,无论是否考虑搜索:
```bash
mnemodoc-server prompt-hook --config /path/to/project/.mnemodoc.yml || true
```
它基于**余弦相似度**,而不是融合的搜索分数 - 后者是排名神器,对于非主题查询和有针对性的查询,顶级命中是相同的。`hook.similarity_threshold` 决定是否注入;`hook.margin_threshold` 决定注入多少,亚军在该距离内意味着排名不确定;`hook.max_passages` 限制集合。每个默认值都在基准测试语料库上衡量,而不是随机选择的 - `mise bench:tokens` 报告命中率、非主题提示的错误命中率以及注入的集合是否实际包含答案。重新校准每个语料库。
每个失败路径都是静默的,并且设计为退出代码 0:这在用户面前同步运行,并且如果钩子在无关的回合中出错,比说 nothing 更糟糕。
`mnemodoc-server info --licenses` 打印二进制文件中内置的第三方许可文本 - 当二进制文件单独重新分发时,其静态链接的依赖项需要这些许可。
### 环境覆盖
每个设置都可以在不编辑 YAML 的情况下重写,这使得 systemd 单元或容器镜像是可配置的:`MNEMODOC_OLLAMA_HOST`、`MNEMODOC_OLLAMA_MODEL`、`MNEMODOC_OLLAMA_TIMEOUT`、`MNEMODOC_OLLAMA_BATCH_SIZE`、`MNEMODOC_SEARCH_TOP_K`、`MNEMODOC_SEARCH_MODE`、`MNEMODOC_SEARCH_BACKEND`、`MNEMODOC_SEARCH_RECENCY_DAYS`、`MNEMODOC_SEARCH_RECENCY_BOOST`、`MNEMODOC_SEARCH_KEYWORD_WEIGHT`、`MNEMODOC_QDRANT_URL`、`MNEMODOC_QDRANT_API_KEY`、`MNEMODOC_QDRANT_COLLECTION`、`MNEMODOC_HOOK_THRESHOLD`、`MNEMODOC_HOOK_MARGIN`、`MNEMODOC_HOOK_MAX_PASSAGES`、`MNEMODOC_SERVER_SSE_HOST`、`MNEMODOC_SERVER_SSE_PORT`、`MNEMODOC_SERVER_LOG_FILE`、`MNEMODOC_SERVER_LOG_LEVEL`、`MNEMODOC_SERVER_DAEMON`、`MNEMODOC_SERVER_IDLE_TIMEOUT`、`MNEMODOC_SERVER_DAEMON_WATCH`、`MNEMODOC_SERVER_WATCH_INTERVAL`、`MNEMODOC_DB_PATH`、`MNEMODOC_INDEX_CONCURRENCY`、`MNEMODOC_INDEX_PDF`、`MNEMODOC_INDEX_MAX_FILE_SIZE`、`MNEMODOC_CHUNKING_STRIP_LINK_ONLY_LINES`、`MNEMODOC_CHUNKING_MERGE_PREAMBLE`、`MNEMODOC_EXCLUDE`。
布尔值接受 `true/1/yes/on`,不区分大小写。无法读取的值 - 非数字计数、无法识别的布尔值 - 将被启动验证报告为每个其他配置问题,并且不会被静默替换为零,而是保留先前的值。
**`ingest_path` 拒绝两件事**,这两件事都保护索引而不是严格限制。拒绝 `paths:` 下列出的根目录之外的路径:索引语料库也是代理读取的内容,因此文档要求索引 `~/.aws/credentials` 否则会成功 - 将目录添加到 `paths:` 使其可索引。并且,如果配置的嵌入模型不再与存储的向量构建的模型匹配,则拒绝部分摄取:这会使所有向量无效,因此索引必须整体重建,而不是从一个路径部分填充。
**分块噪声减少(可选)** - 文档以导航块(链接面包屑和一行描述)打开的文档,否则会将该前言转换为富含关键词但无答案的块,从而占据 `top_k` 槽。`chunking:` 下的两个通用、配置驱动的选项(两者默认为 `false`,因此索引在没有它们的情况下保持不变)解决了这个问题:`strip_link_only_lines` 删除仅由内联链接和分隔符组成的行(例如 `← [Index](…)` - `[Map](…)`),在分块前保留任何带有真实文本的行 - 它涵盖了基于行的标记格式,这些格式将原始标记输入到块中(Markdown、Org-mode `[[…][…]]`、AsciiDoc `link:`/`xref:`/`<…>>`/URL、reStructuredText `` `text <url>`_ ``),并且对 DOM/Office 格式(HTML、`.docx`、`.odt`、EPUB 等)故意不操作(使用 `merge_preamble_into_first_section` 进行这些操作);`merge_preamble_into_first_section` 将标题前言折叠到第一个部分的块中,而不是将其作为独立项发出。更改任一选项后重新索引(运行 `ingest_path` 或完全重新索引)以使新块生效。
**配置路径相对于配置文件解析** - `.mnemodoc.yml` 中的 `doc/claude/` 相对于包含配置文件的目录解析,而不是进程工作目录。将配置文件移动,路径也会随之移动。
**索引位置** - 默认情况下,索引位于 `.mnemodoc/index.db`,在保存配置文件的目录中。该目录还保存 SQLite WAL 文件和守护进程的套接字和锁定,并且在创建时接收一个自我忽略的 `.gitignore`,因此其中没有任何内容可以意外提交。 将索引放在项目内使其可发现,与项目生命周期绑定,并允许其在项目目录重命名或移动后存活。设置 `db.path` 将数据库放在其他位置 - 当项目目录只读、位于 Dropbox/iCloud 同步的文件夹中(可以损坏 WAL 模式下的 SQLite 数据库)或被 `git clean -xdf` 删除时很有用。显式 `db.path` 按原样使用,不会收到 `.gitignore`。
**模型不匹配** - 如果更改配置文件中的 `ollama.model`,请在查询之前重新索引。不同模型的向量具有不兼容的维度,并且会静默地获得接近零的分数。`query_documents` 在检测到不匹配时会在响应中发出 `warning` 字段。
**流式摄取** - 支持进度报告的 MCP 客户端可以通过 `Accept: text/event-stream` 和 `tools/call ingest_path` 请求发送。服务器流式传输每个索引文件的 `notifications/progress` 事件,随后是最终结果帧。在请求参数中包含 `_meta.progressToken` 以接收进度通知:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "ingest_path",
"arguments": {
"path": "/your/docs",
"_meta": { "progressToken": "my-token" }
}
}
}
```
## 上下文角色和 PreToolUse 钩子
大多数文档-RAG 服务器都停留在“代理可以搜索文档”的层面上。问题在于:**代理只检索它已经知道要查找的内容**。它从未听说过的约定是它永远不会查询的约定 - 并且按需检索是上下文窗口填满时首先牺牲的内容。最需要强制执行的约定恰恰是那些悄悄溜过去的约定。
mnemodoc-server 通过一个**角色选择引擎**弥补了这一差距,该引擎通过两个通道共享一个代码路径:
- **按需(在会话中)** - `get_project_context` MCP 工具。代理调用它并采用返回的角色。方便,但仅在代理认为调用时触发。
- **机械(在会话外)** - `mnemodoc-server context` CLI 命令,设计用于由 Claude Code **`PreToolUse` 钩子**驱动。钩子在每次 `Edit`/`Write` 之前作为子进程运行,不在代理的认知循环中,因此在代理请求之前会注入正确的约定。因为钩子在会话外运行,所以它不能调用 MCP 工具 - CLI 通过命令行通道为它提供相同的选择引擎。
相同的引擎,两个入口点。您获得可靠的自动指导和显式的按需查找,而无需保持同步的第二套选择逻辑。
### 定义角色
在 `.mnemodoc.yml` 中添加一个 `context:` 部分。每个角色都指向一个 Markdown 文件(其说明)以及三个轴上的触发器列表 - 编辑的文件、任务类型和用户查询。角色路径相对于配置文件,如 `paths`。
```yml
context:
# 可选的默认项,当没有规则触发并且没有信号进行仲裁时。
default: doc/claude/roles/generalist.md
roles:
- file: doc/claude/roles/backend.md
description: 后端约定 — 操作、持久性、策略
when_files: ["app/concepts/**", "app/models/**", "app/policies/**"]
when_task: ["implement", "refactor"]
when_query: ["operation", "policy", "persistence"]
- file: doc/claude/roles/frontend.md
description: 刺激控制器、Turbo Streams、HAML 视图
when_files: ["app/frontend/**", "app/views/**"]
when_query: ["stimulus", "turbo", "view"]
```
**选择算法.** 规则命中次数会被评分(文件 ×3,任务 ×2,查询 ×1)。 明确的赢家 - 超过置信阈值并领先亚军一个边缘 - 直接获胜。 当规则不明确时,引擎不会猜测:它会嵌入包(文件 + 任务 + 查询)并通过与每个角色描述的余弦相似度来打破僵局。 如果没有信号, 它会回退到 `default`。 结果在规则果断时快速确定,而在不确定时则智能嵌入。
### 连接钩子
在项目的 `.claude/settings.json` 中注册一个 `PreToolUse` 钩子(钩子脚本本身位于您的项目中,而不是在此仓库中):
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "bin/mnemodoc-hook" }
]
}
]
}
}
```
现在钩子只需要 **将有效载荷直接输送到 CLI** - `--hook-stdin` 标志使 `mnemodoc-server context` 读取客户端的钩子 JSON,派生编辑的文件(PreToolUse)或用户查询(UserPromptSubmit),并在其审计日志中记录源会话/代理。CLI 在任何选择失败时都以非零退出;钩子脚本的 `|| true` 包装器吸收该退出代码,确保静默降级,以便缺少 RAG 永远不会警告或阻止编辑。
```sh
#!/usr/bin/env sh
# bin/mnemodoc-hook — 将钩子有效载荷转发到角色选择器。
# `|| true` 使钩子保持沉默:失败的选择(无角色,无信号,Ollama 宕机)
# 绝不能引发警告或阻止编辑。
mnemodoc-server context --hook-stdin --config .mnemodoc.yml || true
```
对于 Claude Code 以外的客户端,请传递 `--client <name>`(今天仅 `claude-code` 交付)或保留显式标志形式作为可移植的备用方案:`mnemodoc-server context --files <path> --config .mnemodoc.yml`.
### 完整设置示例
[`examples/`](examples/) 有两个轴的复制粘贴设置指南:每个 **AI 客户端**(Claude Code、GitHub Copilot、Cursor、Windsurf、Zed - 包括每个客户端支持的层的容量矩阵)和每个 **框架**(Rails、Laravel、Symfony、Django - `.mnemodoc.yml` 角色映射)。选择每个。 从 [`examples/README.md`](examples/README.md) 开始。
## 部署
### systemd
要作为 systemd 服务(SSE 模式)运行,请创建 `/etc/systemd/system/mnemodoc-server.service`:
```ini
[Unit]
Description=mnemodoc-server
After=network.target
[Service]
Type=notify
ExecStart=/usr/local/bin/mnemodoc-server serve --sse --config /path/to/.mnemodoc.yml
Restart=on-failure
WatchdogSec=30
[Install]
WantedBy=multi-user.target
```
```sh
systemctl daemon-reload
systemctl enable --now mnemodoc-server
```
服务器在启动索引传递完成后发送 `READY=1`,在 `SIGTERM` 时发送 `STOPPING=1`。通过 `logrotate` 支持使用 `SIGUSR1` 进行日志轮换。
HTTP 传输还公开了 `GET /health` - 一个轻量级的活动探针,返回 `200 OK`。在 `ExecStartPost` 运行状况检查或负载均衡器探针中使用它。
## 开发
需要:Crystal、mise、Ollama(本地或 Docker)。`sqlite-vec` 向量扩展是一个 git 子模块,因此请使用 `--recurse-submodules`(或在现有检出中运行 `git submodule update --init`)克隆后构建。
```sh
git clone --recurse-submodules <repo-url>
mise dev:ollama # 启动 Ollama(macOS 本地,Metal GPU)+ 拉取模型
mise dev:deps # 安装依赖
mise dev:spec # 运行测试
mise dev:check # 构建 + 校验 + 测试
```
参见 [CLAUDE.md](CLAUDE.md) 了解完整的开发指南。
### 衡量节省
`mise bench:tokens` 量化了项目的核心主张:按需获取相关段落的成本低于每次会话启动时加载整个文档集。
```
单位 :确切(count_tokens,模型 claude-opus-5)
基准 :1411(整个语料库,每次会话加载)
检索 :平均 302.8,中位数 314.5(前 5 名)
节省 :78.5 %
召回率 :18 个问题中的 94.4%
```
**如果不报告节省,就没有意义**,因为这将是无意义的:返回空结果是 100% 节省,但完全失败。基准测试运行一个手动注释的问题集 - 每个问题都指明保存答案的文件和标题 - 并衡量搜索是否在前 K 名中返回该段落。召回率为零的运行结果打印为 **无意义**,并以非零退出,因此没有脚本会将其误认为良好结果。
它针对 `bench/corpus/` 下提交的固定语料库运行,而不是针对真实文档:来自人工语料库的绝对数字意义不大,但它们的**版本之间的演变**是可比较的,这就是使基准测试作为回归防护措施有用的原因。
计数使用 Anthropic 的 `count_tokens` 端点,该端点免费且单独于消息创建限速。 将 `ANTHROPIC_API_KEY` 放入 git 忽略的 `.env`(mise 加载)以获取确切的令牌计数。 没有密钥,基准测试仍然运行,计数字符而不是令牌,并说明输出 - 比例仍然有意义,绝对值不是令牌。 无论哪种方式,都需要 Ollama,因为 Ollama 必须在搜索之前嵌入语料库。
```sh
mise bench:tokens # 人类可读的报告
mise bench:tokens -- --json # 机器可读
mise bench:tokens -- --top-k 3 # 更少的段落/问题
mise bench:tokens -- --model claude-sonnet-5 # 另一个分词器
```
## 贡献
欢迎贡献。请参阅 [CLAUDE.md](CLAUDE.md) 以获取完整的开发指南。
两种搜索信号现在都支持索引,不再随着语料库线性扩展:语义搜索使用 **vec0 KNN 索引** (`sqlite-vec`,固定上游子模块,静态链接)和关键词搜索使用 **SQLite FTS5 / BM25 索引**。两种方法都不会将整个语料库加载到 RAM 中 — 只有匹配文件的数据块按需加载。目前没有突出的扩展工作;在添加更多功能前请先评估性能。
## 许可证
MIT
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器