跳到正文
主页
返回

MCP项目实践-DailySearch

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/listprompts/list暴露可引用上下文或提示模板

2. Builtin 的优缺点是什么?为什么使用 MCP?

2.1 Builtin 是什么

Builtin 指在 FastAPI 后端用 Python @tool 装饰器 + builtin_registry 直接注册、在进程内执行的函数。

2.2 优点

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

  1. 统一协议:服务端 MCP(Docker 内 fetchtavily)与客户端 MCP(插件内 obsidian)都用 tools/list + tools/call,Catalog 里 source 可标为 mcp / client_mcp
  2. 新增能力 ≈ 配置 + Skill:接入社区 MCP(如 mcp-server-fetch)或自研 packages/obsidian-mcp-server不必改 tool_agent 主循环
  3. 边界清晰:云端做网页抓取;本机 Obsidian 做 vault 读写——MCP 统一的是调用方式,不是强行合并运行环境。

4. 本项目中 MCP 解决了哪些原有问题?

4.1 云端无法直接调用用户本机 CLI

问题:FastAPI 跑在 Docker 或远程机器时,无法执行用户电脑上的 obsidian CLI,也无法访问用户 vault 的真实路径。

MCP 解法

测试中的真实帧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 解法

4.3 双份 CLI 白名单与 client_mcp__ 伪协议

问题(迁移文档「现状 vs 目标」表):

MCP 解法

4.4 客户端工具超时

问题:插件未响应时 Agent 一直等。

项目举例test_client_tool_bridge.pytest_timeouttimeout_s=0.05 时返回 {"ok": false, "error": "client tool timeout"}——说明桥接层必须处理超时,而不是假设本机永远在线。


5. 云端 / Docker 使用 MCP 统一前后端的优势

维度无 MCP(Builtin + 双端 CLI)有 MCP
工具发现代码里硬编码工具列表tools/list 同步到 GET /catalog,带 sourcemcp_server_id
Docker 网页能力defuddle 常不可用mcp_fetch__fetchmcp_tavily__* 在容器内 stdio 启动
插件 vault 能力后端假装能读 vaultmcp_client__obsidian__*,必须插件 WebSocket 在线
配置多个环境变量(OBSIDIAN_TOOL_MODEDEFUDDLE_BIN…)收敛为 MCP_ENABLEDMCP_SERVERSTAVILY_API_KEY;用户还可 PUT /settings/mcp
测试难以 mock 双端MCP_ENABLED=0 跑单测;mock mcp_call 帧测桥接

统一的不是运行位置,而是调用契约:云端与用户端仍分割(见下文架构图),但 Agent、Skill、Catalog 只认「限定工具名 + JSON 参数」。


6. 云端 + 用户端:MCP 整体结构

6.1 逻辑架构(迁移目标)

6.2 一次 vault 读笔记的端到端流程(与 docx 提纲一致)

  1. 云端 Agent 根据 Skill obsidian_cli 决定调用 mcp_client__obsidian__vault_read_note,参数 { "path": "Daily/2026-05-20.md" }
  2. tool_agent 识别为 client-side 工具(is_client_side_toolmcp_client__ 前缀)。
  3. ClientToolBridge 组装 mcp_call 帧,经 WebSocket 发给插件。
  4. 插件 wsChat.ts 收到 mcp_call,交给 PluginMcpHostObsidianMcpHostvault_read_note
  5. Obsidian API 在用户本机读 vault 文件。
  6. 结果 JSON 经 mcp_result 回传,tool_agent 继续生成回复。

要点

6.3 插件连接条件


7. MCP Host 可以解决什么问题?

7.1 核心问题:没有 Host 时的「双份维护」

原先:云端 Agent 与插件为了能执行同一套 Obsidian CLI 规则,需要在 PythonTypeScript 各维护一份白名单和解析逻辑(obsidian_tools.py vs obsidianCliRunner.ts)。

有 Host 之后

7.2 Host 还解决的问题

问题Host 的做法
写权限失控vault_write_note 需用户在插件设置显式开启 writeEnabled
工具名混乱统一 server id obsidian,Catalog 限定名 mcp_client__obsidian__vault_read_note
扩展编辑器能力可增 editor_get_active_noteeditor_get_selection(API 级,而非 CLI)

8. WebSocket 通信结构

WebSocket 会话时序:插件经 /ws/chat 发送 turn,流式接收 delta,必要时通过 mcp_call / mcp_result 读写 vault,最终 done 返回 answer

8.1 帧类型演进

阶段请求响应说明
遗留tool_invoke + tool_nametool_resultclient_mcp__vault_read
目标mcp_call + server_id + method + paramsmcp_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

8.4 与会话其他消息的关系


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.pysync_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=obsidianmcp_invoke

9.3 Skill 层如何挂接

Skill典型 required_tools
defuddlemcp_fetch__fetch
obsidian_climcp_client__obsidian__vault_read_note
news_searchfetch_news(builtin)+ 可选 mcp_tavily__*

Skill 在匹配时注入 prompt 片段;tool_agent 主循环不变,只在边界做 MCP 路由。



分享本文:

上一篇
Harness 工程(二):仓库与状态管理
下一篇
Harness 工程(一):五子系统概览