Table of contents
Open Table of contents
1. 什么是 MCP?
MCP(Model Context Protocol) 是面向 AI Agent 的工具扩展标准协议。它约定了一套与具体业务无关的交互方式:
| 能力 | 典型方法 | 作用 |
|---|---|---|
| 工具发现 | tools/list | 列出当前 MCP Server 暴露的工具名、描述、JSON Schema |
| 工具调用 | tools/call | 用结构化 JSON 参数执行某个工具,返回标准结果 |
| 资源 / 提示(可选) | resources/list、prompts/list | 暴露可引用上下文或提示模板 |
2. Builtin 的优缺点是什么?为什么使用 MCP?
2.1 Builtin 是什么
Builtin 指在 FastAPI 后端用 Python @tool 装饰器 + builtin_registry 直接注册、在进程内执行的函数。
2.2 优点
- 延迟低:同进程调用,无子进程或跨机 RPC。
- 上手快:写一个 handler、注册进 registry 即可给 LLM 用。
- 适合零依赖小工具:例如
get_current_time这类高频、无子进程开销的工具(迁移计划中也建议长期保留)。
2.3 缺点(本项目里真实踩过的坑)
| 问题 | 项目中的具体表现 |
|---|---|
| 每加一个工具都要改 Python handler | 每类能力都要在 backend/app/tools/ 写实现并注册,与 Skill 的 required_tools 强耦合。 |
| 双端重复实现 | Obsidian vault 曾在 运行端 与 云端 各维护一套 CLI 白名单与解析逻辑,行为容易不一致。 |
| 环境强依赖 | 早期通过 Defuddle CLI 子进程 抓取网页正文。在本地开发机可以工作,但Docker / 云端部署 时镜像内往往没有 defuddle 可执行文件;子进程、编码、网络策略在容器里更难统一。。 |
| 非标准“伪 MCP” | 早期的 client_mcp__* + clientMcpHost.ts 是手写 RPC,不是 JSON-RPC 2.0 的 tools/call,扩展时要改 orchestrator.ts 里按 tool_name 分支的代码。 |
| 三套执行模式难维护 | 历史上的 OBSIDIAN_TOOL_MODE(client / local / auto)让「工具到底在哪跑」不清晰,排错成本高。 |
2.4 为什么改用 MCP
- 统一协议:服务端 MCP(Docker 内
fetch、tavily)与客户端 MCP(插件内obsidian)都用tools/list+tools/call,Catalog 里source可标为mcp/client_mcp。 - 新增能力 ≈ 配置 + Skill:接入社区 MCP(如
mcp-server-fetch)或自研packages/obsidian-mcp-server,不必改tool_agent主循环。 - 边界清晰:云端做网页抓取;本机 Obsidian 做 vault 读写——MCP 统一的是调用方式,不是强行合并运行环境。
4. 本项目中 MCP 解决了哪些原有问题?
4.1 云端无法直接调用用户本机 CLI
问题:FastAPI 跑在 Docker 或远程机器时,无法执行用户电脑上的 obsidian CLI,也无法访问用户 vault 的真实路径。
MCP 解法:
- 结构化工具名:
mcp_client__obsidian__vault_read_note - 参数为 JSON:
{ "path": "Daily/2026-05-20.md" }(见obsidian_cli.skill.md),禁止再传obsidian read file="..."这类 CLI 字符串。 - 后端
ClientToolBridge.mcp_invoke经 WebSocket 发mcp_call,插件 PluginMcpHost 用 Obsidian App API 读文件,结果经mcp_result返回。
测试中的真实帧(backend/tests/test_client_tool_bridge.py):
{
"type": "mcp_call",
"server_id": "obsidian",
"method": "tools/call",
"params": {
"name": "vault_read_note",
"arguments": { "path": "a.md" }
}
}
4.2 云端误用「本机绝对路径」读 vault
问题:若在后端 Builtin 里用 open("/Users/xxx/vault/note.md"),在 Docker 里路径不存在或根本不是用户的库。
MCP 解法:
- 路径语义统一为 相对 vault 根(如
Projects/foo.md)。 - 实际 IO 只在插件进程内完成(
packages/obsidian-mcp-server的vault_read_note等)。 - 后端与插件不再解析 CLI 字符串,降低路径注入与解析分歧风险。
4.3 双份 CLI 白名单与 client_mcp__ 伪协议
问题(迁移文档「现状 vs 目标」表):
- Python 与 TypeScript 各维护 Obsidian CLI 规则;
tool_invoke+ 工具名字符串,每加一个工具要改orchestrator.ts分支。
MCP 解法:
- 自研
dailysearch-obsidian(packages/obsidian-mcp-server/),安全策略集中在security.ts(allowed_operations、writeEnabled默认关)。 - WebSocket 从
tool_invoke演进到mcp_call/mcp_result,载荷对齐 JSON-RPC 语义。 - 遗留名
client_mcp__vault_read仍兼容,但 Skill 与 README 要求优先mcp_client__obsidian__*。
4.4 客户端工具超时
问题:插件未响应时 Agent 一直等。
项目举例:test_client_tool_bridge.py 的 test_timeout 在 timeout_s=0.05 时返回 {"ok": false, "error": "client tool timeout"}——说明桥接层必须处理超时,而不是假设本机永远在线。
5. 云端 / Docker 使用 MCP 统一前后端的优势
| 维度 | 无 MCP(Builtin + 双端 CLI) | 有 MCP |
|---|---|---|
| 工具发现 | 代码里硬编码工具列表 | tools/list 同步到 GET /catalog,带 source、mcp_server_id |
| Docker 网页能力 | defuddle 常不可用 | mcp_fetch__fetch、mcp_tavily__* 在容器内 stdio 启动 |
| 插件 vault 能力 | 后端假装能读 vault | 仅 mcp_client__obsidian__*,必须插件 WebSocket 在线 |
| 配置 | 多个环境变量(OBSIDIAN_TOOL_MODE、DEFUDDLE_BIN…) | 收敛为 MCP_ENABLED、MCP_SERVERS、TAVILY_API_KEY;用户还可 PUT /settings/mcp |
| 测试 | 难以 mock 双端 | MCP_ENABLED=0 跑单测;mock mcp_call 帧测桥接 |
统一的不是运行位置,而是调用契约:云端与用户端仍分割(见下文架构图),但 Agent、Skill、Catalog 只认「限定工具名 + JSON 参数」。
6. 云端 + 用户端:MCP 整体结构
6.1 逻辑架构(迁移目标)
6.2 一次 vault 读笔记的端到端流程(与 docx 提纲一致)
- 云端 Agent 根据 Skill
obsidian_cli决定调用mcp_client__obsidian__vault_read_note,参数{ "path": "Daily/2026-05-20.md" }。 tool_agent识别为 client-side 工具(is_client_side_tool→mcp_client__前缀)。ClientToolBridge组装mcp_call帧,经 WebSocket 发给插件。- 插件
wsChat.ts收到mcp_call,交给 PluginMcpHost →ObsidianMcpHost→vault_read_note。 - Obsidian API 在用户本机读 vault 文件。
- 结果 JSON 经
mcp_result回传,tool_agent继续生成回复。
要点:
- MCP Host:在插件进程内,把结构化调用转成 Obsidian API,并执行
security.ts中的读写策略。 - WebSocket:只负责把后端的「调用 obsidian 服务器的
vault_read_note」送到插件并带回结果;WS 本身不读文件。
6.3 插件连接条件
7. MCP Host 可以解决什么问题?
7.1 核心问题:没有 Host 时的「双份维护」
原先:云端 Agent 与插件为了能执行同一套 Obsidian CLI 规则,需要在 Python 与 TypeScript 各维护一份白名单和解析逻辑(obsidian_tools.py vs obsidianCliRunner.ts)。
有 Host 之后:
- 业务规则与安全策略集中在
packages/obsidian-mcp-server(例如TOOL_SECURITY、writeEnabled默认false)。 - 插件侧
PluginMcpHost进程内嵌ObsidianMcpHost,对外暴露listTools/callTool。 - 后端只发标准
mcp_call,不再解析obsidian read ...字符串。
7.2 Host 还解决的问题
| 问题 | Host 的做法 |
|---|---|
| 写权限失控 | vault_write_note 需用户在插件设置显式开启 writeEnabled |
| 工具名混乱 | 统一 server id obsidian,Catalog 限定名 mcp_client__obsidian__vault_read_note |
| 扩展编辑器能力 | 可增 editor_get_active_note、editor_get_selection(API 级,而非 CLI) |
8. WebSocket 通信结构

8.1 帧类型演进
| 阶段 | 请求 | 响应 | 说明 |
|---|---|---|---|
| 遗留 | tool_invoke + tool_name | tool_result | 如 client_mcp__vault_read |
| 目标 | mcp_call + server_id + method + params | mcp_result | 对齐 MCP tools/call 语义 |
8.2 mcp_call 示例(与迁移文档一致)
{
"type": "mcp_call",
"turn_id": "...",
"call_id": "...",
"server_id": "obsidian",
"method": "tools/call",
"params": {
"name": "vault_read_note",
"arguments": { "path": "folder/note.md" }
}
}
8.3 插件侧处理(wsChat.ts)
- 无
mcpCallHandler时返回:插件未注册 MCP 处理器。 - 有 handler 时执行
callTool,再通过sendMcpResult回传。
8.4 与会话其他消息的关系
- 用户消息、流式 delta、
done等仍走原有 chat 协议。 - 工具调用是并行通道:bridge 用
call_id匹配 pending Future(见ClientToolBridge.resolve)。
9. MCP 架构:Adapter → 注册 → MCP Server
9.1 服务端(Docker / 本机后端)
McpServerConfig (环境变量 / PUT /settings/mcp)
↓
McpServerSession.connect() # stdio 子进程
↓
McpToolAdapter.sync_into(registry) # tools/list → 注册 ToolDef
↓
限定名例如 mcp_fetch__fetch、mcp_tavily__tavily-search
↓
tool_agent 调用 handler → session.tools/call
关键代码:backend/app/services/mcp/adapter.py 的 sync_into 为每个 MCP 工具生成 mcp_{serverId}__{toolName} 并建立 routing 表。
9.2 客户端(Obsidian 插件)
PluginMcpHost (in-process)
↓
@dailysearch/obsidian-mcp-server / ObsidianMcpHost
↓
vault_* / editor_* 工具
↓
WebSocket mcp_call / mcp_result 与后端 ClientToolBridge 对接
后端 client_proxy 负责把 mcp_client__obsidian__* 映射到 server_id=obsidian 的 mcp_invoke。
9.3 Skill 层如何挂接
| Skill | 典型 required_tools |
|---|---|
defuddle | mcp_fetch__fetch |
obsidian_cli | mcp_client__obsidian__vault_read_note 等 |
news_search | fetch_news(builtin)+ 可选 mcp_tavily__* |
Skill 在匹配时注入 prompt 片段;tool_agent 主循环不变,只在边界做 MCP 路由。