架构总览
🧠 用户 Telegram →
#笔记 搜 Docker🤖 Hermes Gateway → Agent Loop → mcp_tool.py
MCP stdio: stdin→JSON-RPC · stdout←响应
🔌 trilium-mcp v1.0.1 7 tools · Node.js · Authorization: <裸 token> (源码硬编码,无 Bearer 前缀)
HTTPS ETAPI: axios → REST · 环境变量来源:config.yaml env → dotenv(.env)
🗄️ Trilium Server trilium.atibm.com · 1133 笔记
安装路径
/opt/data/
├── home/
│ └── trilium-mcp/ # MCP 服务器(从 github.com/h30190/Trilium_MCP 克隆)
│ ├── dist/index.js # 编译后入口
│ ├── src/ # TypeScript 源码
│ │ ├── index.ts # 主入口,dotenv 加载 ../.env
│ │ └── trilium-client.ts # axios 客户端,第 11 行硬编码 Authorization
│ ├── .env # ETAPI Token(dotenv 自动读取)
│ ├── node_modules/
│ ├── package.json
│ └── tsconfig.json
├── config.yaml # mcp_servers.trilium 配置
└── .env # Hermes 主环境变量
源码细节
入口 (dist/index.js)
// dist/index.js 第 13 行 — dotenv 自动加载 ../.env
dotenv.config({ path: path.resolve(__dirname, "../.env") });
const TRILIUM_ETAPI_TOKEN = process.env.TRILIUM_ETAPI_TOKEN;
环境变量加载优先级
- config.yaml mcp_servers.trilium.env — Hermes 启动 MCP 子进程时注入环境变量
- MCP 自带 .env — dotenv 加载(不会覆盖已存在的环境变量)
- 两者都配了同一个 token。如果某天不一致,config.yaml 的注入优先级更高
Authorization 头格式(⚠️ 踩坑点)
// dist/trilium-client.js 第 8 行 — 直接传 token,无 Bearer 前缀
headers: {
'Authorization': token, // ← 裸 token,没有 "Bearer "
'Content-Type': 'application/json',
},
已验证:atibm.com 这个 Trilium 实例只接受裸 token 格式。
- ✅
Authorization: TaCxTq...5Yo=→ 200 OK - ❌
Authorization: Bearer TaCxTq...5Yo=→ 401 NOT_AUTHENTICATED
验证测试(对齐 MCP 实际行为)
# 必须先用 source 加载 .env(终端子进程不会自动读取 .env 文件)
source /opt/data/.env
# ✅ 正确:裸 token,与 MCP 源码一致
curl -s -X GET "https://trilium.atibm.com/etapi/notes/root" \
-H "Authorization: ${TRILIUM_ETAPI_TOKEN}"
# ❌ 错误:Trilium 实例会返回 401
# curl -H "Authorization: Bearer ${TRILIUM_ETAPI_TOKEN}" ...
一次对话安装
帮我装一下 Trilium MCP,安装到 /opt/data/home/trilium-mcp,用
https://github.com/h30190/Trilium_MCP 这个仓库。装完在 config.yaml
的 mcp_servers 里配上,env 里引 .env 的 TRILIUM_ETAPI_TOKEN 就行。
最后 /restart。
注:安装后记得检查 MCP 自带的 .env 文件中的 token 是否与 /opt/data/.env 一致。
使用方式
🔍 搜索笔记
触发 search_notes(query)
| 你说 | Hermes 执行 |
| 搜索我的笔记里关于 Docker 的内容 | search_notes("Docker") |
| 搜一下 gbrain 相关的笔记 | search_notes("gbrain") |
📖 读取笔记
触发 read_note(noteId) — 返回基础元数据(noteId/title/type/mime/date)+ 正文
| 你说 | Hermes 执行 |
| 读一下笔记 ludkdAFlaQi5 的内容 | read_note("ludkdAFlaQi5") |
| 打开刚才搜到的那篇笔记 | 将上一步的 noteId 传给 read_note |
📋 获取元数据
触发 get_note_metadata(noteId) — 完整属性/标签/关系/子笔记树
| 你说 | Hermes 执行 |
| 查看这篇笔记的标签和属性 | get_note_metadata(id) → attributes/labels/relations/childNoteIds |
✏️ 创建笔记
触发 create_note(parentNoteId, title, content, type, mime)
| 你说 | Hermes 执行 |
| 创建一篇新笔记,标题叫"测试" | create_note(parentNoteId="...", title="测试", type="text") |
| 创建一篇 Mermaid 图表笔记 | create_note(..., type="mermaid") |
🔄 更新笔记
触发 update_note(noteId, title?, content?, type?, mime?)
| 你说 | Hermes 执行 |
| 把标题改成"已修改" | update_note(id, title="已修改") |
| 在末尾追加一段话 | read_note → 拼接 → update_note |
📂 移动笔记
触发 move_note(noteId, parentNoteId) — ⚠️ 会替换所有父节点,克隆笔记会丢失其他父位置
| 你说 | Hermes 执行 |
| 移到 Hermes 配置目录下 | get_note_metadata 检查是否克隆 → move_note |
🏷️ 管理属性
触发 manage_attributes(action, noteId, type, name, value, isInheritable)
| 你说 | Hermes 执行 |
| 加标签 status:done | manage_attributes("create", noteId, "label", "status", "done") |
| 改为 status:review | 先查 attributeId → manage_attributes("update", attributeId, value="review") |
| 关联到另一篇笔记 | manage_attributes("create", noteId, "relation", "relatedNote", targetId) |
故障排查
| 现象 | 原因 | 排查 |
| curl 裸 token 401,带 Bearer 也 401 | Token 本身无效 | 在 Trilium 设置页面重新生成,同步更新 MCP 的 .env 和 /opt/data/.env |
| curl 裸 token 通,MCP 不通 | MCP 子进程没读到正确的 token | 检查 MCP 自带 .env 的 token 是否正确;检查 config.yaml env 段是否正确引用 \${TRILIUM_ETAPI_TOKEN};重启 MCP(/restart) |
| curl 带 Bearer 通,MCP 不通 | Trilium 要求 Bearer 前缀但 MCP 发裸 token | 修改 trilium-client.ts 第 11 行或在 token 值前硬编码 "Bearer " |
| 找不到工具 / tools 列表为空 | MCP 未注册或启动失败 | ps aux | grep trilium-mcp · 检查 config.yaml mcp_servers 段 · /restart 重启 |