关于 Notes Vault
Notes Vault 是面向使用 Markdown 笔记库的 MCP 服务,可连接本地目录或 S3 存储。它通过本地索引、Schema 校验和 Claude Code 会话钩子,帮助 Agent 检索、维护笔记并发现其与代码的偏差。
主要功能
- 01本地 SQLite 索引与 BM25 搜索
- 02按 Schema 校验笔记写入
- 03笔记归档与仓库日志追加
- 04lint 检查笔记状态与链接
- 05Claude Code 会话上下文钩子
适用场景
- 01检索已有笔记和历史决策
- 02按规则创建或更新 Markdown 笔记
- 03开始会话时查看相关系统笔记
- 04检查过期笔记及未记录的提交
MCP 相关
作为 MCP 服务,Notes Vault 向客户端提供笔记搜索、读取、写入、整理和检查工具。笔记库存于本地目录或 S3 存储。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
uvx notes-vault-mcpVAULT_PATHDirectory holding the vault. Set this or the four S3_* variables.S3_ENDPOINTS3 or MinIO endpoint URL holding the vaultS3_ACCESS_KEY密钥Access key for the vault bucketS3_SECRET_KEY密钥Secret key for the vault bucketS3_BUCKETBucket holding the vaultS3_PREFIXKey prefix inside the bucketS3_REGIONRegion for the vault bucketVAULT_CACHE_DIRWhere the SQLite index livesVAULT_SCHEMALocal schema file overriding the one in the vaultVAULT_TOKEN密钥Bearer token required by the streamable HTTP transportVAULT_STOP_HOOKSet to off to disable the stop hook本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
notes-vault-mcp
用于 Markdown 笔记库的 MCP 服务——也就是 Obsidian 保存笔记的那种形式:由带有 YAML frontmatter 的 .md 文件组成的文件夹。笔记库可以位于本地目录或 S3 存储桶中(也支持 MinIO),该服务为 Agent 提供一种低成本、带索引的读取和写入方式。
这样做的目的是让 Agent 能通过一次调用回答“关于这件事,我们已经知道些什么?”,并在笔记库与代码不一致时得到提醒。因此,该服务不只会读写文件:
- 本地 SQLite 索引。 每次调用工具都会刷新索引,只获取版本发生变化的笔记。搜索时不会下载整个笔记库。
- 支持 BM25 排序的全文搜索,结合文件夹权重、时间衰减和状态因子,因此,仍在使用的系统笔记会比两年前归档、但包含相同词语的计划排名更高。
- 一个模式。 frontmatter 规范存放在笔记库的
.vault/schema.yml中:有哪些文件夹及其用途、哪些字段是必填的、哪些状态和类型有效、哪些文件夹必须关联一个area。写入内容会根据该规范进行验证,不符合规范时将被拒绝。 - 生命周期管理。
close会归档已完成的笔记并标记其状态;log_append会为每个仓库的每个会话写入一条带日期的记录;lint会报告发现的所有不一致。 - 适用于 Claude Code 的会话钩子:
session-start会向 Agent 提供它即将操作的仓库对应的系统笔记,以及每条笔记上次更新后产生的提交;如果会话结束时有提交未记录或笔记已过时,stop会拒绝结束会话。
瑞典语和英语笔记都适用:索引会折叠变音符号,并对笔记库中的所有语言进行词干提取,因此搜索 äppelpressar 会找到 äppelpressen,搜索 pressing 会找到 presses;模式还包含同义词列表,因此搜索 bokning 会找到 booking。
安装
作为 Claude Code 插件
claude plugin marketplace add https://github.com/gronare/claude-plugins
claude plugin install vault@gronare
插件会询问笔记库设置,并将其作为 CLAUDE_PLUGIN_OPTION_* 环境变量传入;该服务会像读取原始变量名一样读取这些变量。
直接从 PyPI 安装为 MCP 服务
claude mcp add vault -s user \
-e VAULT_PATH=$HOME/vault \
-- uvx notes-vault-mcp
或者使用 S3 / MinIO:
claude mcp add vault -s user \
-e S3_ENDPOINT=https://minio.example.com \
-e S3_ACCESS_KEY=... \
-e S3_SECRET_KEY=... \
-e S3_BUCKET=vault \
-- uvx notes-vault-mcp
作为容器
claude mcp add vault -s user -- \
docker run --rm -i \
-e S3_ENDPOINT -e S3_ACCESS_KEY -e S3_SECRET_KEY -e S3_BUCKET \
ghcr.io/gronare/notes-vault-mcp:latest
配置
每个变量也会从 CLAUDE_PLUGIN_OPTION_<NAME> 读取,这是 Claude Code 插件传递用户配置的方式。如果两者都已设置,则优先使用原始变量名。
| 变量 | 必填条件 | 含义 |
|---|---|---|
VAULT_PATH | 使用本地笔记库时必填 | 存放笔记库的目录。选择本地后端。 |
S3_ENDPOINT | 使用 S3 笔记库时必填 | 端点 URL,例如 https://minio.example.com。 |
S3_ACCESS_KEY | 使用 S3 笔记库时必填 | 访问密钥。 |
S3_SECRET_KEY | 使用 S3 笔记库时必填 | 秘密密钥。 |
S3_BUCKET | 使用 S3 笔记库时必填 | 存放笔记库的存储桶。 |
S3_PREFIX | 否 | 存储桶内的键前缀。 |
S3_REGION | 否 | 区域,默认为 us-east-1。 |
VAULT_CACHE_DIR | 否 | 索引的存放位置,默认为 ~/.cache/notes-vault-mcp。 |
VAULT_SCHEMA | 否 | 模式文件的本地路径,用于覆盖笔记库中的模式。 |
VAULT_TOKEN | 使用 --auth bearer 时必填 | 用于 --transport http 局域网模式的静态 bearer 令牌。 |
VAULT_ALLOWED_HOSTS | 否 | 服务器会响应的额外 Host 请求头值,以空格或逗号分隔。 |
VAULT_STOP_HOOK | 否 | 设置为 off 可禁用停止钩子。 |
设置 VAULT_PATH 或四个 S3_* 变量。两者都未设置时,服务会退出并输出一行说明。
首次运行
uvx notes-vault-mcp init
init 会向笔记库写入内容,且如果未使用 --force,不会覆盖任何已有内容:
.vault/schema.yml— frontmatter 规范,从内置默认值复制而来,供你编辑。Areas.base、Open tasks.base、Resources.base、Backlog.base、Log.base— 基于相同结构创建的 Obsidian Bases 视图。
随后,它会将一段 CLAUDE.md 代码片段输出到 stdout:Agent 在对话中需要遵循的工作流规则。
架构
.vault/schema.yml 会与内置默认配置进行深度合并,因此只需包含不同之处。
默认配置定义了五个文件夹:
| 文件夹 | 类型 | 权重 | 作用 |
|---|---|---|---|
Areas/ | system | 3.0 | 每个系统一篇持续更新的笔记。仅记录当前状态。图谱的枢纽。 |
Resources/ | reference | 2.0 | 陷阱、操作指南,以及附有原因说明的决策。 |
Projects/ | task | 1.0 | 跨会话的未完成工作。使用 close 关闭。 |
Log/ | log | 1.0 | 每个代码仓库一篇只追加内容的日志笔记。 |
Archive/ | archive | 0.3 | 历史记录。仅在请求时搜索。 |
以及笔记的约定:
frontmatter:
required: [title, date, updated, tags, status]
optional: [kind, area, summary, path, superseded_by, priority, source, kelpie]
area_required_in: [Projects, Resources, Log]
status_values: [draft, active, complete, superseded]
kind_values: [system, task, trap, howto, decision, reference, log]
每种类型都附有一行规则,说明何时笔记属于该类型;服务会将这些规则写入指令,因此每个客户端选择类型的方式都相同:
kinds:
trap: "a gotcha that bit once and will bite again; nothing to do, nothing was chosen"
decision: "a choice and its reasons; something was chosen, alternatives were dropped"
howto: "steps that are known to work; nothing chosen, nothing open"
知识库可以通过添加条目(research: "reading notes on a topic; no task, no decision")来新增类型,通过重新表述规则来改写规则,并将类型设为 null 来删除类型。即使只有 kind_values 而没有规则,也仍然可用。
path 用于将笔记与代码关联:它是以逗号分隔的目录列表(~ 会按原样保留,同时也会以展开后的形式建立索引)。context 和会话钩子会据此进行匹配。
每个代码仓库对应一篇仓库日志笔记,其文件名和行格式都由架构设置决定:
log:
folder: Log
file_format: "{repo}-log.md"
entry_format: "- [{date}] {line} | commits: {commits} | {area}"
file_format 接受一个 {repo} 占位符;默认后缀用于避免日志与枢纽笔记重名:如果知识库中同时存在 Areas/myapp.md 和 Log/myapp.md,Obsidian 就无法解析 [[myapp]]。所有构建日志路径的地方都会读取此设置——log_append、context 中的日志尾部内容、停止钩子对未记录提交的检查、changelog 和 lint——因此更改此设置会一次性调整所有相关路径。更改设置时,请将现有文件重命名为匹配的名称。
language 是知识库的主要语言,other_languages 则是其余语言,使用 ISO 代码表示(language: sv、other_languages: [en]);搜索会对 Snowball 支持的每种语言进行词干提取,更改这些设置会重建索引。没有匹配项时,回复会告知客户端知识库使用哪些语言编写,以便客户端翻译搜索词并再次搜索;部分匹配结果也会用一行文字说明相同信息。仅通过词干匹配的结果会乘以 stemmed_weight(默认为 0.5,低于正文的 1.0),因此精确形式会排在前面。
还可以配置:标签词汇表及是否强制执行、搜索会扩展的同义词组、stale_after_days,以及搜索权重。
工具
每次调用都会先刷新索引,最多每 20 秒刷新一次。
| 工具 | 成本 | 功能 |
|---|---|---|
search | 低 | 对索引进行全文搜索。搜索文件名主体、标题、摘要、标签和正文,并支持同义词、前缀、词干提取、带引号的短语和变音符号折叠。查询内容若与某条笔记的文件名主体完全一致(或为 [[stem]]),则会将该笔记置于首位,即使它来自归档。查询中的每个词都必须匹配;若没有笔记包含所有词,则会返回匹配词数最多的笔记,并在标题中说明结果不完整。仅输入提交 SHA 会找到提及该 SHA 的笔记。隐藏归档和已取代的笔记,并说明隐藏的数量。 |
context | 低 | 会话开始时调用:一次性返回涵盖某路径的系统笔记、开放任务、stale_after_days 天以上的开放笔记分流列表(含每条笔记的三种处理方式)、待办事项、参考笔记和仓库日志末尾内容。 |
list_files | 低 | 仅返回路径。 |
read_file | 中 | 读取一条笔记,前缀为 etag: <version>。已取代的笔记会附带警告提示。 |
obsidian_access | 写入 | 仅限 --auth forwarded。生成 Obsidian Remotely Save 插件通过 WebDAV 使用的个人令牌,并仅显示一次;新令牌会撤销之前的令牌。 |
lint | 中 | 读取每条笔记并报告不一致之处。 |
init | 写入 | 初始化一个知识库:在根目录写入 .vault/schema.yml 和 Obsidian Bases 视图,并保留已有内容;force 会覆盖。插件的 /vault:init 会调用此工具。 |
write_file | 写入 | 根据架构验证内容,不符合要求时拒绝写入。写入 updated 时间戳,并补上 date。传入 expected_etag 可进行条件写入,传入 "*" 则用于创建必须尚不存在的笔记。在 S3 上,条件会随 PUT 请求一同发送(If-Match、If-None-Match: *);若服务拒绝条件 PUT,则会先执行 HEAD 再执行 PUT,并且只记录一次该情况。只会写入 .md 笔记,且不会写入点号开头的文件夹;架构和 .base 视图由 init 创建,read_file 仍可提供这些文件。 |
append_file | 写入 | 追加内容并更新 updated;笔记必须存在且仍符合架构。 |
set_status | 写入 | 更改笔记状态而不重写笔记:设为 backlog 并指定优先级会将其搁置,设为 active 则会重新启用。 |
close | 写入 | 将状态设为已完成(或在提供 merged_into 时,设为已取代并填写 superseded_by),并将笔记移入归档。 |
log_append | 写入 | 在仓库日志中追加一条带日期的记录,并注明由此产生的提交。日志不存在时会创建日志。 |
move_file | 写入 | 移动或重命名笔记;笔记在目标位置必须符合架构。 |
delete_file | 写入 | 永久删除。优先使用 close。 |
search 过滤条件:folder、status、tag、kind、area、path_prefix、since、
include_archive、include_superseded、limit。
lint 报告内容
broken_frontmatter、missing_required(按字段分别报告)、missing_area、unknown_tags(仅在词汇表严格时)、unresolved_links、orphans(没有入链 wikilink;忽略日志和归档)、stale_active、archive_status_mismatch、duplicate_stems、superseded_target_missing。使用 --park-stale 时,超过 stale_after_days 天未更新的开放任务笔记会在生成报告前被设为 backlog(优先级 low,来源 veckolint <date>),并列在 parked 下,因此 active 始终表示在该时间窗口内有过更新;如需重新启用,使用 set_status(path, "active")。若笔记包含一条声明(kelpie 键),且该声明是在最近 14 天内创建或刷新的,则会保留其当前状态;时间从 since 和笔记 updated 两者中较晚的日期起算。
uvx notes-vault-mcp lint
uvx notes-vault-mcp lint --write "Log/lint-$(date +%F).md"
uvx notes-vault-mcp lint --write Log/lint.md --park-stale
Hooks
两个 Claude Code hook 都会从 stdin 读取 hook JSON,并且无论发生什么都会以 0 退出。
session-start 会打印工作目录的上下文包,然后针对它返回的每条系统备注,列出自该备注上次更新以来触及其 path 的提交。在 Agent 采信备注之前,这能回答“这条备注还准确吗?”。
stop 会阻止未记录工作的会话结束:列出过去 24 小时内的提交中,其 sha 未出现在仓库日志里的提交;以及会话在其记录中通过 vault 工具调用读取或写入的、超过架构 stale_after_days 天数仍未关闭的任务备注。它会返回 {"decision": "block", "reason": ...};如果 vault 已保持最新,则不返回任何内容。会话未触及的过期备注留给下一次会话处理:context 会在 triage 下列出这些备注,以及逐条处理它们的三种方式(关闭、添加带日期的行,或搁置到待办事项列表),而不是每次停止时都处理。将 VAULT_STOP_HOOK=off 设为静默该 hook;例如,在会话设置的 env 中,运行 Agent 会话并自行写入仓库日志的工具就可以这样做。
{
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "uvx notes-vault-mcp hook session-start" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "uvx notes-vault-mcp hook stop" }] }
]
}
}
其他命令
notes-vault-mcp serve --transport stdio # the default
notes-vault-mcp sync --rebuild # drop the index and read every note again
notes-vault-mcp search "bokning" --limit 5 # the same ranking, from a shell
notes-vault-mcp changelog myapp 2026-08 --repo-path ~/projects/myapp
notes-vault-mcp changelog myapp 2026-08 --repo-path ~/projects/myapp --write
notes-vault-mcp changelog --all # this month (and last month during its first week)
changelog 会打印日志行、按日期分组的 git 提交,以及日期处于该时间段内的仓库备注。使用 --write 时,它会将这些内容保存为时间段页面 Log/<repo>-<period>.md,放在标记 和 之间;标记上方的文字(由 Agent 在月末撰写的摘要)会保持不变,页面的状态则随日历变化。--all 会对本机上 session-start hook 见过的每个仓库执行此操作;stop hook 每天会运行一次,因此无需 cron,页面也能保持最新。
待办事项列表
notes-vault-mcp backlog --area myapp --priority high
待办事项是状态为 status: backlog 的任务备注,包含 area、单行 summary、可选的 priority(urgent、high、medium、low)和可选的 source(说明是谁在何时提出,或提供 sha)。backlog_add 会在对话中有事项被推迟时立即为其创建一条记录;backlog 会按优先级再按时间列出这些事项,并可按 area 或 family 筛选;除了未完成任务外,context 还会显示与当前仓库相关的待办事项。要着手处理某个事项,只需将其状态设为 active;完成后则执行 close。无论待办事项备注有多旧,Lint 都不会处理它们;但如果某条 complete 备注从未关闭,则会发出提示。init 会在其他 Obsidian bases 旁写入 Backlog.base。
任务和日志命令
适用于驱动编码 Agent 并在固定节点(开始、推送、完成、重置)自行记录事实,而不依赖 Agent 调用 MCP 工具的工具。每条命令都会在 stdout 上打印一个 JSON 对象并以 0 退出;失败时以非零状态退出,并在 stderr 上打印一行错误信息。所有写入都通过与 MCP 工具相同的路径:架构验证、updated 时间戳和索引。
notes-vault-mcp task open --name "Fix login" --cwd ~/wt/wt1.myapp [--branch fix-login]
# {"path": "Projects/myapp-fix-login.md", "created": true, "repo": "myapp", "area": "myapp"}
notes-vault-mcp task open --note Projects/myapp-idea.md --cwd ~/wt/wt1.myapp
# {"path": "Projects/myapp-idea.md", "created": false, "repo": "myapp", "area": "myapp"}
notes-vault-mcp task check Projects/myapp-fix-login.md --need problem,solution,decisions
# {"missing": ["solution", "decisions"]}
notes-vault-mcp task close Projects/myapp-fix-login.md # {"path": "Archive/myapp-fix-login.md"}
notes-vault-mcp task backlog Projects/myapp-fix-login.md --priority medium # {"path": "Projects/…"}
notes-vault-mcp task claim Projects/myapp-idea.md --machine mac --task fix-login --repo myapp [--force]
# {"path": "Projects/myapp-idea.md", "claimed": true, "claim": {"machine": "mac", "task": "fix-login", "repo": "myapp", "since": "2026-10-23T09:12:00Z"}, "replaced": null}
notes-vault-mcp task release Projects/myapp-idea.md [--machine mac]
# {"path": "Projects/myapp-idea.md", "released": true, "claim": null}
notes-vault-mcp task release --machine mac --task fix-login
# {"paths": ["Projects/myapp-renamed.md"], "released": true}
notes-vault-mcp log append --cwd ~/wt/wt1.myapp --line "Login fixed" --commit abc1234 --commit def5678
# {"path": "Log/myapp-log.md"}
notes-vault-mcp log path --repo myapp # {"path": "Log/myapp-log.md"}
notes-vault-mcp status
# {"configured": true, "version": "0.5.1", "backend": "s3", "writable": null}
notes-vault-mcp status --probe
# {"configured": true, "version": "0.5.1", "backend": "s3", "writable": true}
仓库名取自主检出目录的名称,即使从 worktree 执行也是如此;区域则是会话开始钩子为该目录加载的系统备注,如果没有匹配的备注,则回退为仓库名(输出中无匹配备注时 "area": null)。task open 会写入一条任务备注,以任务名作为占位标题,状态为 active,path 指向主检出目录,source: branch <name>,各节标题下的内容为空;文件名主干由仓库名和任务名组成,并确保在整个知识库中唯一。--note 则认领一条现有任务备注:状态设为 active,其他内容不变。--branch 默认为 --cwd 中检出的分支。
task claim 会在任务备注的 kelpie frontmatter 键中记录哪台机器正在处理哪个任务(machine、task、repo、UTC 时间的 since),这样同一知识库中的第二台机器就能看到该备注已被占用。它只修改该键,并基于读取到的版本执行条件写入;如果期间备注发生变化,则会重新读取。若备注由另一台机器持有,则保持原样,并返回 "claimed": false 以及该机器的认领信息;--force 会接管,并将旧认领信息作为 replaced 返回。同一台机器再次认领同一任务时,会保留 since。task release 会移除此键;指定 --machine 时,如果认领属于另一台机器,就会保留该认领并返回 "released": false。如果未提供路径,--machine 和 --task 会在该任务当前所在位置的所有备注中清除其认领信息,即使备注已移动或重命名;"paths": [] 表示备注已不存在,其认领信息也随之消失。close 和将状态改为 backlog 也会移除认领信息;write_file 则会保留已存储的认领信息,不受文件内容影响,因此只有这些命令会更改认领信息。请在架构的 optional 中列出 kelpie,并在服务端说明中告知客户端不得编辑此键。
task check 使用规范键 problem、solution、decisions、tests 和 scope。如果某个节的标题下、下一个同级或更高级标题前有任何文本,就视为该节已填写。标题由架构按语言设置,并根据知识库的 language 选择:
task:
sections:
en: { problem: Problem, solution: Solution, decisions: Decisions, tests: Test decisions, scope: Out of scope }
sv: { problem: Problem, solution: Lösning, decisions: Beslut, tests: Testbeslut, scope: Utanför ramen }
status 不会失败:未配置知识库时,它会打印 "configured": false, "backend": null 并以 0 退出。此命令只读,并将 writable 留为 null;--probe 会通过写入并删除 .vault/write-probe 来检测可写性。
这些命令会优先从环境变量读取存储设置。如果环境中未指定知识库,则会改用 Claude Code 插件保存的用户配置:$CLAUDE_CONFIG_DIR/settings.json(默认为 ~/.claude/settings.json)中的 pluginConfigs["vault@<marketplace>"].options,并将 vault_path、s3_endpoint、s3_bucket、s3_access_key、s3_secret_key、s3_prefix 和 s3_region 映射到对应变量。Claude Code 保存在该文件之外的值必须通过环境变量提供。
HTTP 传输
VAULT_TOKEN=$(openssl rand -hex 32) notes-vault-mcp serve --transport http --host 0.0.0.0 --port 8765
在 /mcp 上使用 Streamable HTTP,支持四种身份验证方式:--auth bearer(默认)、--auth oidc、--auth builtin 和 --auth forwarded。
bearer 是局域网模式。每个请求都必须携带 Authorization: Bearer $VAULT_TOKEN;否则会在到达服务之前收到 401。此模式下必须设置 VAULT_TOKEN,否则命令将拒绝启动。这是一个拥有完整访问权限的单一令牌,claude.ai 无法使用它。
Host 标头的处理取决于服务的监听地址。绑定到 localhost 时,只响应 localhost、127.0.0.1 和 [::1],其他请求都会收到 421;绑定到其他地址时,则响应所有 Host。如果要在保留此检查的同时使用自定义域名、Kubernetes Service 或局域网主机名,请将其列入 VAULT_ALLOWED_HOSTS,或设置 VAULT_PUBLIC_URL:这样会响应该名称和 localhost,其他请求则会收到 421。
oidc 和 builtin 使用 OAuth,这是 Claude 连接器所需的方式。请参阅远程访问:claude.ai、Claude Desktop 和移动端。
forwarded 适用于位于已完成 OAuth 验证的代理之后的服务:它信任经过签名的身份标头,并为每个 subject 提供一个保险库。请参阅代理之后:每人一个保险库。
远程访问:claude.ai、Claude Desktop 和移动端
使用 --auth builtin 或 --auth oidc 通过 HTTPS 运行服务,claude.ai 就可以将其添加为自定义连接器。在网页上连接一次后,同一连接器也会出现在 Claude Desktop 和移动应用中。
一个服务实例提供一个保险库。
前提条件
- 一个保险库:具有专用密钥的兼容 S3 存储桶,或主机上的文件夹。
- Docker,或主机上的
uv。 - 一个指向该机器的域名,且服务前方配置了 TLS。
deploy/下的示例使用 Caddy,它会自动获取证书。 - 对于
--auth oidc:一个已在运行的 OpenID Connect 提供商。
两种模式
--auth builtin | --auth oidc | |
|---|---|---|
| 登录者 | 一位所有者,使用此服务保存的密码登录 | 提供商允许的任何人 |
| 客户端注册 | 动态注册,无需在 claude.ai 中配置 | 提供商支持时动态注册,否则需将客户端 ID 和密钥粘贴到 claude.ai |
| 只读客户端 | 登录页面上的两个复选框 | 群组成员资格 |
| 磁盘上的状态 | VAULT_AUTH_DIR 下的 auth.sqlite | 无 |
环境
| 变量 | 模式 | 含义 |
|---|---|---|
VAULT_PUBLIC_URL | oidc, builtin | 必填。客户端访问的 https 地址;如果服务不在根路径,还需包含路径。无默认值:未设置时服务会拒绝启动,并且拒绝任何不是 https:// 的地址(本地试用时可使用 http://localhost)。 |
VAULT_AUTH_DIR | builtin | auth.sqlite 的存放位置——其中包含所有者的密码哈希、已注册的客户端和经过哈希处理的令牌。默认值为 ~/.cache/notes-vault-mcp/auth。 |
VAULT_OIDC_ISSUER | oidc | 必填。提供方的颁发者 URL。 |
VAULT_OIDC_AUDIENCE | oidc | 提供方在 aud 中填写的客户端 ID。必填;缺少此值或面向其他受众的令牌都会被拒绝。 |
VAULT_OIDC_READ_GROUP | oidc | 授予 vault:read 权限的组。默认值为 vault。 |
VAULT_OIDC_WRITE_GROUP | oidc | 授予 vault:read 和 vault:write 权限的组。默认值为 vault-writers。 |
VAULT_OIDC_SCOPES | oidc | 资源元数据公布的范围,以便客户端向提供方请求其支持的范围。默认值为 openid profile email groups。如果访问令牌不包含 groups,服务会请求提供方的 userinfo 端点。 |
VAULT_TOKEN | bearer | 静态令牌。 |
VAULT_ALLOWED_HOSTS | all | 服务额外响应的 Host 标头值,以空格或逗号分隔,可填写 Service 名称或与公网主机名并列的局域网主机名。 |
VAULT_CACHE_DIR | all | 索引的存放位置。默认值为 ~/.cache/notes-vault-mcp。 |
S3_ENDPOINT、S3_BUCKET、S3_ACCESS_KEY、S3_SECRET_KEY | all | 保管库配置,除非 VAULT_PATH 指定了本地文件夹。 |
MCP 端点为 VAULT_PUBLIC_URL + /mcp。将此 URL 粘贴到 claude.ai 即可。
权限范围
vault:read—search、read_file、list_files、context、lint、backlog。vault:write—write_file、append_file、move_file、delete_file、close、log_append、backlog_add。
使用只读令牌调用写入工具时,会返回错误,说明该令牌只能读取保管库,而不会只写入一部分或静默地什么也不做。
从零开始使用内置登录
cd deploy/caddy-builtin
cp .env.example .env
$EDITOR .env
docker compose up -d
docker compose exec vault notes-vault-mcp owner set-password
然后,在 claude.ai 中依次选择:Settings → Connectors → Add custom connector → 输入 /mcp URL,例如
https://vault.example.com/mcp。无需其他操作:服务会自行注册客户端。
浏览器会打开服务的登录页面,要求输入所有者密码,并显示两个复选框:读取和写入。取消勾选“写入”,即可创建只读连接器。
查看已连接的客户端,以及如何断开连接:
docker compose exec vault notes-vault-mcp tokens list
docker compose exec vault notes-vault-mcp tokens revoke <client_id>
密码哈希和令牌存放在 vault-data 卷中的 auth.sqlite 文件里。请备份该卷;如果丢失,每个客户端都必须重新连接。
从零开始使用 OIDC
cd deploy/caddy-oidc
cp .env.example .env
$EDITOR .env
docker compose up -d
在此配置中,服务是资源服务器:它会验证提供方的访问令牌,且永远不会接触密码。写入组中的用户拥有 vault:read 和 vault:write 权限,读取组中的用户仅拥有 vault:read 权限,两者皆不属于的用户会被拒绝。
以相同方式在 claude.ai 中添加连接器。如果提供方支持动态客户端注册,配置到此就完成了。如果不支持(Pocket ID、Authentik),请先在提供方注册客户端,然后在连接器的 Advanced 设置中粘贴客户端 ID 和密钥;在提供方中允许的重定向 URI,应为 claude.ai 在该对话框中显示的 URI。
Pocket ID
claude.ai 会将 MCP 资源(/mcp URL)作为 OAuth resource 参数发送,而 Pocket ID 只接受已知为 API 的资源。因此:
- Settings → APIs → Add API:设置名称和资源标识符,准确填写
VAULT_PUBLIC_URL+/mcp。添加两个权限:键为vault:read和vault:write,名称填写家人将在授权页面上看到的内容。 - Settings → User Groups:创建
vault(读取)和vault-writers(写入),并将用户添加到相应组中。 - Settings → OIDC Clients → Add:名称为
claude.ai,回调 URL 为https://claude.ai/api/mcp/auth_callback,启用 PKCE,关闭 public client,允许的用户组设为vault。在 API access 下,授予该 API 的两项权限(用户委托)。复制客户端 ID 和密钥。 - 在服务器上:
VAULT_OIDC_ISSUER是 Pocket ID 的基础 URL,VAULT_OIDC_AUDIENCE是第 1 步中的资源标识符(Pocket ID 会将其放入aud),VAULT_OIDC_SCOPES为openid profile email groups vault:read vault:write。 - 在 claude.ai 中,在 Advanced settings 下使用客户端 ID 和密钥添加连接器。
用户所属的组决定令牌可以执行哪些操作;只有在提供方完全没有报告组信息时(无论是在令牌中还是通过 userinfo),客户端权限才会起作用。
Kubernetes
deploy/k8s/ 中包含原始清单:一个将 PVC 挂载到 /data 的 Deployment、一个 Service、一个 Ingress 和一个 Secret 示例。
cp deploy/k8s/secret.example.yaml secret.yaml
$EDITOR secret.yaml
$EDITOR deploy/k8s/ingress.yaml
kubectl apply -f secret.yaml -f deploy/k8s/deployment.yaml -f deploy/k8s/service.yaml -f deploy/k8s/ingress.yaml
Deployment 使用 --auth builtin;如需使用 OIDC,请更改 --auth 参数,并在 Secret 中填写 VAULT_OIDC_* 键。就绪探针会访问 /.well-known/oauth-protected-resource/mcp,这是两种模式下唯一一个无需令牌即可响应的路由;如果 VAULT_PUBLIC_URL 包含路径,探针 URL 中也要包含该路径。Pod 启动后设置所有者密码:
kubectl exec -it deploy/notes-vault-mcp -- notes-vault-mcp owner set-password
填写 ingressClassName,并将 secretName 指向 TLS 证书——可以是现有密钥,也可以是 cert-manager 签发的证书。
通过代理:每人一个保险库
--auth forwarded 适用于这样的组织:组织已在 MCP 服务前运行 OAuth 授权服务器,并在标头中将调用者身份作为签名令牌转发(Cloudflare Access 使用的模式是 Cf-Access-Jwt-Assertion)。服务会用代理的公钥验证该令牌,从中获取主体,并提供该主体自己的保险库:在同一个存储桶中为每个人设置一个前缀(或在 VAULT_PATH 下为每个人设置一个文件夹),每个保险库都有自己的索引、自己的 .vault/schema.yml 和一条欢迎说明,这些内容都会在首次请求时创建。无需手动配置任何内容;在代理处授予访问权限即可完成整个入门流程。
notes-vault-mcp serve --transport http --host 0.0.0.0 --port 8765 --auth forwarded
与代理的约定:没有有效身份的请求会收到 403,且不带 WWW-Authenticate 挑战(在这里公布授权服务器会让客户端绕过代理);传输是无状态的,使用普通 JSON 响应,因此缓冲代理无需 Mcp-Session-Id;不会检查 Host 标头;/up 无需令牌即可响应 ok,供就绪探针使用。此模式下不会提供 OAuth 元数据路由;这些路由由代理负责。
| 变量 | 含义 |
|---|---|
VAULT_PUBLIC_URL | 必填。客户端访问服务的地址;其主机名是令牌必须携带的 aud,其路径(如有)会作为所有路由的前缀。 |
VAULT_IDENTITY_PUBLIC_KEY | 必填。代理的 PEM 公钥(RS256 或 ES256),可以是原始格式,也可以是单行 base64。 |
VAULT_IDENTITY_ISSUER | 必填。代理令牌中的 iss。 |
VAULT_IDENTITY_HEADER | 携带令牌的标头。默认为 X-Forwarded-Identity。 |
VAULT_IDENTITY_TYP | 设置后,令牌的 typ 必须匹配。 |
VAULT_IDENTITY_WRITE_CLAIMS | 授予 vault:read 和 vault:write 权限的授权名称(位于令牌的 entitlements 列表或 scope 中)。默认为 vault:write。 |
VAULT_IDENTITY_READ_CLAIMS | 仅授予 vault:read 权限的授权名称。默认为 vault:read。不具备这两类授权的令牌会收到 403 insufficient_scope。 |
VAULT_SUBJECT_PREFIX | 保险库在存储桶或文件夹中的存放位置:<prefix>/<sub>/。默认为 users。 |
VAULT_AUTH_DIR | personal.sqlite 存放个人令牌哈希值的位置。 |
VAULT_CACHE_DIR | 每个主体的 SQLite 索引都存放在此处;请为其配置一个卷。 |
令牌必须包含 iss、aud、sub 和 exp;若存在 email 和 name,则会使用它们。不是普通标识符的主体会先经过哈希处理,再用于命名文件夹。
通过同一服务使用 Obsidian
每个保险库也会通过 VAULT_PUBLIC_URL/dav/ 提供 WebDAV 服务,供无法使用 OAuth 的 Obsidian Remotely Save 插件使用。用户让 Claude 运行 obsidian_access;服务会生成一个个人令牌,只返回一次,并且只存储该令牌的哈希值。Remotely Save 使用 /dav/ 地址、任意用户名(通常使用邮箱)以及作为密码的令牌。再次运行 obsidian_access 会轮换令牌。代理必须直接转发 /dav/*,并完整保留客户端的 Authorization 标头以及 WebDAV 方法和标头(PROPFIND、MKCOL、MOVE、COPY、Depth、Destination、Overwrite)。首次请求时写入的欢迎说明会包含这些操作指引。
开发
uv sync
uv run pytest
uv run ruff check .
测试套件使用 tests/fixtures/vault/ 下的样例保险库和由 moto 模拟的 S3 存储桶运行,不会访问真实存储桶。
许可证
MIT。请参阅 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。