当我们决定为一个桌面博客客户端实现 MCP 服务时,面对的不仅是"如何让 AI 调用几个 API"的技术问题,更是一系列关于信任边界、能力暴露、人机协作模式的设计抉择。这篇文章记录了这些抉择背后的思考。
一、为什么是 MCP,而不是 REST API 或插件系统
在 AI 辅助写作的浪潮中,大多数工具选择了两条路径:要么在应用内嵌入一个 AI 聊天窗口,要么暴露一套 REST API 供外部调用。我们都没有选。
内嵌 AI 的问题在于绑定。你被锁死在某个模型、某个提供商、某个交互范式里。当更好的模型出现,当用户有不同的 AI 偏好,内嵌方案就变成了技术债。
REST API 的问题在于定位。博客客户端是一个本地应用,数据存储在用户的文件系统上,不存在"服务器"。为了让 AI 调用而强行启动一个 HTTP 服务,引入了不必要的网络层、认证机制、端口管理 —— 这些复杂性与"在本地管理我的博客"的场景完全不匹配。
MCP 的 stdio 传输方式精准地解决了这个矛盾:AI 客户端直接启动一个本地进程,通过管道通信,没有网络、没有端口、没有认证。 进程随 AI 客户端的生命周期启停,不留后台残余。这不是对 REST 的妥协,而是对本地应用场景的正确抽象。
更重要的是,MCP 是一个协议而非实现。用户可以自由选择 Claude Desktop、Cursor、Claude Code 或任何未来的 MCP 兼容客户端。我们不绑定 AI 提供商,不绑定交互界面,只定义能力边界。博客应用负责"我能做什么",AI 客户端负责"怎么和用户交互"。
二、独立二进制:一个关于耦合的决定
MCP 服务最终以 gridea-pro-mcp 独立二进制的形式存在,而不是作为主应用的子命令(gridea-pro mcp)。这个决定看似是工程细节,实则反映了一个架构原则:运行时独立,代码共享。
Gridea Pro 的桌面应用基于 Wails 构建,打包后是一个包含 GUI 运行时的 .app bundle。MCP 服务不需要窗口、不需要菜单、不需要事件循环 —— 它是一个纯粹的 stdio 进程。将两者打包在一起意味着:
- MCP 进程启动时需要跳过所有 GUI 初始化逻辑
- macOS 的
.appbundle 路径规则给命令行调用带来额外复杂性 - 二进制体积膨胀(包含了永远不会用到的 GUI 依赖)
但独立二进制不意味着独立代码库。gridea-pro-mcp 和桌面应用共享同一套 Service 层和 Repository 层。文章的创建逻辑、标签的自动匹配、闪念的标签提取 —— 这些业务规则只存在一份。当我们修复了 Service 层的一个 bug,MCP 服务同步受益,反之亦然。
这就是"独立二进制,共享代码"的含义:部署形态上解耦,业务逻辑上统一。 两个入口、一套逻辑、一份数据。
三、能力暴露的层次:工具、资源、提示词
MCP 协议定义了三种能力暴露方式,它们不是同一件事的三种写法,而是 AI 与应用交互的三个层次。理解这个层次结构,是理解 Gridea Pro MCP 设计的关键。
工具(Tools)—— 原子操作
工具是最底层的能力单元。create_post 创建一篇文章,delete_tag 删除一个标签 —— 每个工具做且只做一件事。AI 根据用户意图自主决定调用哪些工具、以什么顺序调用。
这里有一个有意思的设计张力:工具应该多细粒度?
太粗粒度(比如一个 manage_blog 工具包揽一切),AI 无法灵活组合;太细粒度(比如把"创建文章"拆成"创建文件"“写入内容"“更新索引”),AI 需要理解不必要的实现细节。
我们的原则是:工具的粒度应该对应用户心智模型中的一个动作。 用户会说"创建一篇文章"“删除一个标签”,不会说"向 posts 目录写入一个 markdown 文件”。工具的边界应该与用户意图的边界一致。
资源(Resources)—— 上下文感知
资源是 AI 主动获取的上下文信息。gridea://site/info 告诉 AI 这个博客叫什么名字、谁是作者;gridea://posts/summary 让 AI 快速了解博客的全貌。
资源的价值不在于"提供数据"—— 工具也能做到。资源的价值在于让 AI 具备上下文感知能力。当用户说"帮我写一篇技术博客"时,AI 不是在真空中写作。它知道这个博客的主题倾向、现有的标签体系、最近的内容趋势。这种感知让 AI 的输出从"通用"变成"贴合"。
提示词模板(Prompts)—— 工作流编排
提示词模板是最高层的抽象。blog_writing_assistant 不是一个工具,而是一套工作流:先查阅现有标签和分类,再拟定大纲,再撰写全文,最后发布。它编排了多个工具的调用顺序和决策逻辑。
提示词模板体现了一个核心理念:把专家知识固化为可复用的流程。
一个熟练的 Gridea Pro 用户知道,写完文章后应该检查标签是否已存在、分类是否合理、文件名是否规范。新用户不知道这些。提示词模板弥合了这个认知差距 —— AI 按照模板中的专家流程执行,即使用户只说了一句"帮我写篇博客"。
三个层次的关系可以这样理解:
提示词模板 → 定义"做什么"和"怎么做"的流程
↓ 编排
工具 → 执行每一步具体操作
↑ 参考
资源 → 提供决策所需的上下文
四、安全设计:信任的梯度
让 AI 操作用户数据,最敏感的问题不是技术实现,而是信任。Gridea Pro MCP 的安全设计基于一个前提:信任不是二元的开关,而是连续的梯度。
第一层:读操作自由,写操作谨慎
查看文章列表、读取站点配置 —— 这些读操作没有副作用,无需确认。但任何写操作都需要经过 AI 的判断,而删除操作则进一步要求用户显式确认。这不是技术限制,而是对操作风险的分级响应。
第二层:两步确认,而非权限模型
我们没有实现传统意义上的权限系统(“AI 可以创建但不能删除”),因为这种静态权限无法适应动态的使用场景。一个用户可能在整理博客时需要批量删除过时文章,另一个用户可能永远不想让 AI 动已有内容。
取而代之的是两步确认机制:AI 首次调用危险操作时,只返回预览(“即将删除《Go 并发编程》,确认吗?");用户确认后才真正执行。这个设计把决策权交还给用户,同时保留了 AI 的操作能力。
更微妙的是,两步确认不仅是安全机制,也是交互设计。它创造了一个自然的对话节点,让用户有机会修正 AI 的理解偏差。“我说的不是这篇文章"比"我不知道 AI 删了什么"好得多。
第三层:能力隔离,默认最小化
部署功能通过环境变量 DEPLOY_ENABLED 控制,默认不注册。这不是简单的功能开关,而是能力隔离的体现 —— 未注册的工具在 MCP 协议层面不存在,AI 既不知道它的存在,也无法尝试调用。
这比"注册但拒绝执行"更安全。后者仍然暴露了工具的存在,AI 可能反复尝试或向用户建议"开启此功能”。完全不注册意味着:能力边界由配置决定,而非由 AI 的自我约束决定。
第四层:敏感信息永不出境
通过 MCP 获取部署设置时,token、password、privateKey 等字段自动替换为 ***。这个处理发生在 MCP 服务内部,在数据序列化为 JSON 之前。敏感信息永远不会进入 AI 的上下文窗口,从根源上消除了泄露路径。
五、数据一致性:两个入口,一份真相
Gridea Pro 的数据存储在本地文件系统中 —— JSON 文件存配置和元数据,Markdown 文件存文章内容。桌面应用和 MCP 服务都直接读写这些文件,没有中间层。
这带来了一个必须正视的问题:两个写入者,一份数据。
我们的做法是让 MCP 服务复用桌面应用完全相同的 Repository 和 Service 代码。不是"类似的逻辑”,不是"参考实现",而是同一份代码。当 Service 层规定"创建闪念时自动提取 #标签",MCP 路径和 GUI 路径走的是同一个 extractTags 函数,用的是同一个 Unicode 正则表达式。
这个决策有一个直接推论:MCP 服务必须与桌面应用在同一个代码仓库中。 我们曾考虑过将 MCP 独立为一个单独的项目,但很快放弃了 —— 两个仓库意味着两份 Domain 模型、两份数据访问逻辑、两套需要同步维护的代码。任何一次 Post 结构的变更都需要在两个地方修改,这在实践中是不可持续的。
同一份代码、同一份数据、同一组业务规则。这是保证一致性的唯一可靠方式。
六、面向意图的接口设计
MCP 工具的接口设计不是"暴露后端 API",而是面向 AI 的意图表达。两者的区别体现在很多细节中。
参数应该符合自然语言的表达
创建文章时,分类参数是 category(单数),不是 categories(复数),也不是 categoryId。因为用户会说"归到’技术’分类下",而不是"关联 category ID a3f8e2"。AI 传入分类名称,Service 层负责匹配已有分类或创建新分类 —— 这个转换对 AI 是透明的。
标签参数接受逗号分隔的字符串("Go, 并发, 编程"),而不是 JSON 数组。因为在自然语言中,标签就是一组逗号分隔的词。
返回值应该服务于决策
list_posts 不返回文章的完整内容(那可能是几万字),只返回元数据(标题、日期、标签、发布状态)。这不是因为性能 —— 虽然确实更高效 —— 而是因为 AI 在"选择操作哪篇文章"的场景下,不需要全文内容。元数据足够支撑决策。
当 AI 确实需要全文时,get_post 提供了这个能力。两个工具、两个场景、两种数据粒度。
工具描述应该指导 AI 的行为
render_site 的描述是 “Render the static site. Call this after making changes to posts or settings.” 后半句不是功能说明,而是使用时机的提示。AI 读到这个描述后,会在创建或修改文章后主动建议"需要渲染站点吗?"。
工具描述是 MCP 中最被低估的设计表面。它不是给人读的 API 文档,而是给 AI 读的行为指导。
七、渐进式能力暴露
不是所有用户都需要所有功能。一个只想用 AI 写文章的用户,不需要(也不应该)面对部署相关的复杂性。
Gridea Pro MCP 通过环境变量实现能力的渐进式暴露:
// 基础配置:内容管理 + 渲染
{
"env": {
"SOURCE_DIR": "/path/to/site"
}
}
// 完整配置:内容管理 + 渲染 + 部署
{
"env": {
"SOURCE_DIR": "/path/to/site",
"DEPLOY_ENABLED": "true"
}
}
这个设计的微妙之处在于:环境变量控制的不是"权限",而是"能力的存在性"。DEPLOY_ENABLED 为 false 时,deploy_site 工具在 MCP 协议层面不存在。AI 的工具列表中没有这个选项,它不会建议用户部署,也不会在工作流中考虑部署步骤。
这比传统的权限控制更优雅。权限控制说"你不能做这件事",能力隔离说"这件事不存在"。前者需要处理拒绝的交互(“为什么不能?怎么开启?"),后者从根本上消除了这个问题。
未来如果需要更多可选能力(比如 SFTP 部署、Netlify 部署、自定义渲染管线),同样的模式可以自然扩展,而不需要引入复杂的权限管理系统。
八、从"工具箱"到"工作伙伴”
MCP 协议的三层能力模型(工具、资源、提示词)暗示了一个更大的设计愿景:AI 不仅是被动的工具执行者,更是主动的工作伙伴。
工具让 AI 能做事。
资源让 AI 了解上下文。
提示词模板让 AI 懂得流程。
三者组合,AI 从"调用 API 的自动化脚本"进化为"理解博客运营的协作者"。它知道创建文章前应该先查阅现有标签体系,知道部署前应该先渲染,知道删除操作需要谨慎确认。这些不是硬编码的 if-else,而是通过工具描述和提示词模板传递给 AI 的领域知识。
这也是我们选择在 MCP 中实现 5 个提示词模板的原因。它们不是"快捷方式",而是博客运营最佳实践的编码化表达。site_health_check 编码了"什么是健康的博客站点"的知识,content_review 编码了"好文章应该具备哪些元素"的标准。这些知识原本只存在于经验丰富的博主脑中,现在通过 MCP 提示词模板,任何用户都可以借助 AI 获得同等质量的运营指导。
结语
Gridea Pro MCP 的设计,归根到底是在回答一个问题:在 AI 时代,一个本地应用应该如何与 AI 协作?
我们的答案是:
- 不嵌入,而是开放 —— 通过标准协议暴露能力,不绑定任何 AI 提供商
- 不复制,而是共享 —— 独立进程、共享代码,保证一致性
- 不信任,而是验证 —— 分层的安全机制,把最终决策权交给用户
- 不罗列,而是编排 —— 从原子工具到工作流模板,传递领域知识而非仅仅暴露功能
这些原则不仅适用于博客客户端,也适用于任何想要接入 AI 生态的本地应用。MCP 协议提供了技术基础,但好的设计决策才是真正决定用户体验的因素。
评论