从零构建 MCP 服务:让 AI 直接操作你的博客

MCP Gridea Pro Go AI

作为一个独立开发者,我一直在想:能不能让 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。理由很简单:

  1. MCP 的本质就是一个无头服务进程
  2. 独立编译体积更小、启动更快
  3. 避免 macOS .app bundle 路径的各种坑
  4. 未来可以独立发布到 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 的站点诊断工具。请执行全面健康检查:

  1. 调用 list_posts — 检查未发布的草稿、缺失日期、空内容
  2. 调用 list_tags — 识别没有关联文章的空标签
  3. 调用 list_categories — 识别空分类
  4. 调用 list_links — 检查缺失 URL 的友链
  5. 调用 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 服务创建并发布 🤖

评论