作为一个独立开发者,我一直在想:能不能让 AI 直接操作我的博客? 不是那种"帮我写个草稿我再复制粘贴"的伪自动化,而是真正的 AI 原生工作流——AI 理解我的站点结构、读我的闪念、帮我组织内容、一键发布。
这篇文章记录了我为 Gridea Pro 实现 MCP (Model Context Protocol) 服务的完整过程。从零到一,28 个 Tools、3 个 Resources、5 个 Prompts,以及那些踩过的坑。
🎯 有趣的是,你正在阅读的这篇文章,就是通过 MCP 服务由 AI 直接创建并发布的。
什么是 MCP?为什么它重要?
MCP 是 Anthropic 推出的开放协议,全称 Model Context Protocol。简单来说,它定义了 AI 和外部工具之间的通信标准——就像 USB 统一了外设接口一样,MCP 统一了 AI 调用工具的方式。
在 MCP 出现之前,每个 AI 应用都在造自己的轮子:OpenAI 有 Function Calling,LangChain 有 Agent Tools,各家互不兼容。MCP 的出现改变了这个局面:
┌──────────────┐ stdio/SSE ┌──────────────┐
│ Claude │ ◄───── MCP ──────► │ Gridea Pro │
│ Desktop │ JSON-RPC 2.0 │ MCP Server │
└──────────────┘ └──────┬───────┘
│
┌─────▼──────┐
│ Service │
│ Layer │
└─────┬──────┘
│
┌─────▼──────┐
│ 文件系统 │
│ (MD/JSON) │
└────────────┘
对于 Gridea Pro 这样的桌面应用,MCP 意味着:你的博客客户端瞬间获得了 AI 超能力,而且不需要改动任何已有代码。
架构决策:独立二进制 vs 子命令
第一个需要做的决策是:MCP Server 应该怎么启动?
方案 A:集成到主程序
把 MCP 功能塞进 Gridea Pro 主程序,通过 gridea-pro mcp 子命令启动。看起来优雅,但有个致命问题——Gridea Pro 基于 Wails 框架,主程序启动会初始化整个 GUI 环境。一个"无头"的 MCP 服务进程并不需要窗口系统。
方案 B:独立二进制
编译一个轻量的 gridea-mcp 二进制。它只包含 Service 层和 MCP 协议处理,不依赖 Wails。
go build -o gridea-mcp ./backend/cmd/mcp
最终选择了 方案 B。理由很简单:
- MCP 的本质就是一个无头服务进程
- 独立编译体积更小、启动更快
- 避免 macOS
.appbundle 路径的各种坑 - 未来可以独立发布到 Homebrew
入口文件简洁到令人愉悦:
func main() {
if _, err := os.Stat(mcp.GetAppDir()); os.IsNotExist(err) {
fmt.Printf("Error: GRIDEA_SOURCE_DIR not found at %s\n", mcp.GetAppDir())
os.Exit(1)
}
server := mcp.NewServer()
if err := server.Start() {
fmt.Printf("Error starting MCP server: %v\n", err)
os.Exit(1)
}
}
核心设计:复用 Service 层
Gridea Pro 的后端架构是经典的分层模式:Domain → Repository → Service → Facade。MCP Server 直接复用 Service 层,零重复业务逻辑。
这意味着一个关键的设计原则:MCP 的 Tool Handler 只做协议转换,不写业务逻辑。
// ❌ 错误示范:在 Handler 里写业务逻辑
func deletePostHandler(s *service.PostService) server.ToolHandlerFunc {
return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 直接操作文件系统... 这不应该出现在这里
os.Remove(filepath.Join(appDir, "posts", filename))
}
}
// ✅ 正确做法:委托给 Service 层
func deletePostHandler(s *service.PostService) server.ToolHandlerFunc {
return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
filename, _ := req.RequireString("filename")
if err := s.DeletePost(ctx, filename); err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText("Post deleted"), nil
}
}
初始化服务时,我们需要手动构建整个依赖图:
func initServices(appDir string) *Services {
// Repository 层
postRepo := repository.NewPostRepository(appDir)
tagRepo := repository.NewTagRepository(appDir)
// ...
// Service 层
tagService := service.NewTagService(tagRepo)
postService := service.NewPostService(postRepo, tagRepo, tagService, mediaRepo)
// Renderer(依赖最复杂)
rendererService := service.NewRendererService(appDir, postRepo, themeRepo, settingRepo)
rendererService.SetMenuRepo(menuRepo)
rendererService.SetLinkRepo(linkRepo)
// ...
}
这里有个有趣的发现:RendererService 的依赖注入用了 Setter 模式而不是构造函数注入。这在 GUI 模式下由 Wails 的 DI 容器管理,但在 MCP 模式下需要手动调用。架构的味道在这里泄露了。 如果有时间重构,应该把所有依赖都放到构造函数里。
危险操作确认机制
让 AI 操作你的博客,最怕的是什么?AI 一个手滑,把文章全删了。
所以我设计了一个"两步确认"机制。所有危险操作(删除、部署、修改配置)都需要 confirm 参数:
func deletePostHandler(s *service.PostService) server.ToolHandlerFunc {
return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
filename, _ := req.RequireString("filename")
confirm := req.GetBool("confirm", false)
if !confirm {
// 第一次调用:返回预览信息
post, _ := s.GetByFileName(ctx, filename)
return mcp.NewToolResultText(fmt.Sprintf(
"⚠️ CONFIRMATION REQUIRED\n"+
"Are you sure you want to delete post '%s'?\n"+
"Call delete_post again with confirm=true to proceed.",
post.Data.Title,
)), nil
}
// 第二次调用(带 confirm=true):真正执行
s.DeletePost(ctx, filename)
return mcp.NewToolResultText("Post deleted"), nil
}
}
对于 update_theme_config 这种修改配置的操作,确认时还会展示变更 diff:
⚠️ CONFIRMATION REQUIRED
The following changes will be applied:
siteName: '述尔' → 'Eric's Blog'
siteDescription: '一个产品经理的碎碎念' → 'Tech & Life'
Call update_theme_config again with confirm=true to apply.
这个设计的精妙之处在于:AI 自己会"读懂"这个预览信息,然后向用户确认是否继续。 人类始终在回路中。
踩坑记录:mcp-go SDK 的版本陷阱
我们使用的是 mark3labs/mcp-go v0.8.x,这是 Go 生态最成熟的 MCP SDK。但在开发过程中踩了不少坑。
坑 1:Prompt 参数 API 变化
// ❌ 错误用法(文档过时)
mcp.WithArgument("tone",
mcp.ArgumentDescription("The tone"),
mcp.ArgumentDescription("casual")) // 默认值不能这样设!
// ✅ 正确用法
mcp.WithArgument("tone",
mcp.ArgumentDescription("The tone (default: casual)"))
ArgumentDescription 不是 DefaultString!这个 API 设计确实容易让人困惑。
坑 2:Resource Handler 返回类型
// ❌ 之前的心智模型:返回单个 ResourceContents
return mcp.ResourceContents{...}, nil
// ✅ 实际需要:返回切片
return []mcp.ResourceContents{
mcp.TextResourceContents{
URI: request.Params.URI,
MIMEType: "application/json",
Text: string(jsonBytes),
},
}, nil
坑 3:Tool 定义函数的参数传递
最初的设计是把 Service 实例同时传给 Tool 定义函数和 Handler 函数:
// 最初的写法
s.mcpServer.AddTool(
listPostsTool(s.services.Post), // Tool 定义也接收 Service?!
listPostsHandler(s.services.Post),
)
但 Tool 定义函数只是构造 JSON Schema,根本不需要 Service 实例。这导致了大量 “unused parameter” lint 警告。修正后:
// 清爽的写法
s.mcpServer.AddTool(listPostsTool(), listPostsHandler(s.services.Post))
教训:先写接口再写实现,不要让惯性驱动设计。
Prompts:从"工具箱"到"智能工作流"
MCP 不只是一个 Tool 集合。通过 Prompts,我们可以把散装的 Tools 编排成有意义的工作流。
比如 site_health_check 这个 Prompt,它告诉 AI:
你是 Gridea Pro 的站点诊断工具。请执行全面健康检查:
- 调用 list_posts — 检查未发布的草稿、缺失日期、空内容
- 调用 list_tags — 识别没有关联文章的空标签
- 调用 list_categories — 识别空分类
- 调用 list_links — 检查缺失 URL 的友链
- 调用 get_theme_config — 检查站点名/描述是否为空
以结构化报告呈现,标注严重级别:🔴 严重 | 🟡 警告 | 🟢 正常
一个 Prompt,串联了 7 个 Tools,输出一份专业的诊断报告。这才是 MCP 的真正威力——不是单打独斗,而是协同作战。
最终成果
| 类型 | 数量 | 说明 |
|---|---|---|
| Tools | 28 | 覆盖文章、闪念、标签、分类、友链、菜单、主题、设置、评论、渲染 |
| Resources | 3 | 站点信息、文章摘要、最近闪念 |
| Prompts | 5 | 写作助手、闪念整理、内容审查、健康检查、文章翻译 |
整个 MCP 模块约 1500 行 Go 代码,没有引入任何新的外部依赖(除了 mcp-go SDK 本身)。
配置只需两步:
{
"mcpServers": {
"gridea-pro": {
"command": "/path/to/gridea-mcp",
"env": {
"GRIDEA_SOURCE_DIR": "/Users/eric/Documents/Gridea Pro"
}
}
}
}
然后你就可以对 Claude 说:
- “帮我把最近的闪念整理成一篇文章”
- “检查我的博客有什么问题”
- “把这篇文章翻译成英文”
- “帮我写一篇关于 Go 并发的技术博客”
AI 会自动调用对应的 Tools,完成整个工作流。
写在最后
MCP 不是银弹。它不会让你的应用变得更好——它只是让你的应用对 AI 可见、可操作、可编排。
但光是这一点,就已经足够改变游戏规则了。
当每个桌面应用都支持 MCP 时,AI 就不再是一个"对话框里的聊天机器人",而是一个真正的数字助手——它能看到你的文件、理解你的数据、执行你的操作,而且在关键时刻还会停下来问你一句:"确定要这样做吗?"
这才是 AI 原生时代应有的样子。
— 本文由 Claude 通过 Gridea Pro MCP 服务创建并发布 🤖
评论