用 MCP 管理 Hugo 博客:这个项目是怎么搭起来的
这个仓库表面上是一个很轻量的 Hugo 博客,实际目标更明确:把博客内容、构建部署、以及 AI 写作入口放在同一个工程里。最终形成的工作流是:文章仍然以 Markdown 存在仓库中,Hugo 负责生成静态站点,仓库流水线负责构建部署,而 mcp/ 目录里的 MCP 服务负责给 AI 提供一组可控的读写工具。
项目目标#
这个项目解决的是一个很具体的问题:写博客时不想每次都手动打开仓库、创建 frontmatter、提交文章、再等待部署。更理想的方式是直接和 AI 对话,让 AI 能够读取已有文章、理解站点配置、创建新文章、修改元数据,必要时还能上传图片。
为了不把 AI 直接暴露给整个文件系统,项目没有设计成“随便执行命令”的形式,而是把能力收敛成 MCP 工具。每个工具只做一件事,例如列文章、读文章、创建文章、更新文章、上传图片。这样既方便使用,也比较容易控制边界。
顶层结构#
当前仓库大致分成几块:
.
├── hugo.toml # Hugo 站点配置
├── content/ # 博客内容
│ ├── about.md
│ └── posts/ # Markdown 文章目录
├── layouts/partials/ # 本站覆盖的局部模板
├── themes/terminal/ # Hugo terminal 主题
├── .github/workflows/ # 构建与部署流水线
└── mcp/ # 自定义 blog MCP 服务
hugo.toml 定义站点基础信息,例如语言、标题、主题、分页和菜单。文章目录使用 Hugo 默认的 content/posts。内容层仍然保持 Hugo 最朴素的方式:每篇文章是一个 Markdown 文件,文件头部用 TOML frontmatter 描述标题、日期、标签、摘要等信息。这样即使以后不使用 MCP,也不会被锁死在某个特殊系统里。
Hugo 站点部分#
Hugo 部分没有做复杂封装,主要依赖 terminal 主题。文章 frontmatter 大致包含这些字段:
+++
title = "文章标题"
date = "2026-04-28T10:57:00+08:00"
tags = ["标签"]
description = "文章摘要"
showFullContent = false
readingTime = false
hideComments = false
+++
这里有几个固定字段值得注意:
showFullContent = false:列表页不直接展开全文。readingTime = false:不显示阅读时间。hideComments = false:保留评论开关字段,后续可以接评论系统。description:作为列表页摘要,也方便 AI 搜索时快速理解文章内容。
这种结构的好处是简单,Hugo 可以直接构建,MCP 也只需要解析 frontmatter 和正文即可。
自动部署链路#
部署由仓库里的 workflow 负责。触发条件围绕站点内容和 Hugo 配置变化设计,MCP 服务自身的开发改动不必每次都触发站点发布。
流水线核心步骤如下:
- 拉取仓库。
- 使用 Hugo Docker 镜像构建静态站点。
- 将生成的
public/目录同步到远端部署目录。 - 成功或失败后发送部署结果通知。
构建阶段没有依赖 runner 本机安装 Hugo,而是通过 Docker 固定构建环境。部署阶段使用同步工具把静态文件发布到服务器,保证服务器上的文件和最新构建结果一致。
失败通知会整理关键上下文和最近日志,方便判断问题发生在 Hugo 构建、依赖准备还是部署同步阶段。这里不在文章中记录真实通知地址、服务器地址、用户名或远端路径。
MCP 服务的定位#
mcp/ 是这个项目最关键的扩展。它是一个 TypeScript 写的 MCP server,包名是 blog-mcp,运行方式可以是:
cd mcp
npm install
npm run build
npm run start
开发时也可以直接运行源码:
npm run dev
它依赖的核心库很少:
@modelcontextprotocol/sdk:实现 MCP server。axios:请求 Gitea API。@iarna/toml:解析和生成 TOML frontmatter。zod:声明 MCP 工具参数。typescript和tsx:开发与构建。
服务启动后通过 stdio 和 AI 客户端通信。AI 客户端看到的不是整个仓库,而是一组明确的工具。
MCP 工具列表#
当前 MCP 服务暴露了 10 个工具:
list_posts 列出文章
read_post 读取指定文章
create_post 创建新文章
update_post 修改文章正文或元数据
delete_post 删除文章
list_tags 统计标签
search_posts 搜索文章
upload_image 上传图片
list_images 列出图片
get_site_config 读取 Hugo 配置
这些工具覆盖了日常写博客的大部分动作:查看已有内容、找历史文章、生成新文章、更新摘要和标签、维护图片素材。它们的颗粒度比较合适,既能让 AI 做事,又不会给 AI 过大的权限面。
Gitea API 封装#
mcp/src/gitea.ts 封装了和 Gitea contents API 的交互。初始化时需要配置 Gitea 地址、访问令牌、仓库所有者和仓库名。文章里只说明需要这些配置项,不记录任何真实值。
可选配置包括请求超时时间、图片上传允许目录、图片大小限制等。它们的作用是控制 MCP 服务的运行边界,而不是把本地机器或私有服务细节暴露出来。
GiteaClient 提供的能力很直接:列目录、读文件、创建文件、更新文件、删除文件,以及针对图片这类二进制文件的 raw base64 上传。所有文本内容在提交到 Gitea 前都会转成 base64,这是 Gitea contents API 的要求。
路径处理也没有直接拼字符串了事,而是把 owner、repo、path 每一段分别编码。这样中文文件名、空格或其他特殊字符不容易把 API 路径弄坏。
Frontmatter 处理#
mcp/src/frontmatter.ts 负责文章元数据。它做了几件事:
- 识别
+++包裹的 TOML frontmatter。 - 解析出
meta和body。 - 重新生成 frontmatter 时保持统一格式。
- 生成固定时区的 ISO 8601 时间。
- 根据标题生成文件名。
- 校验传入日期是否合法。
创建文章时,服务会自动补齐这些字段:
title
date
author
tags
keywords
description
showFullContent
readingTime
hideComments
这保证了 AI 写出来的文章不会因为漏 frontmatter 字段而破坏列表页显示。
路径与图片安全#
mcp/src/paths.ts 是安全边界之一。文章文件名必须是单个 .md 文件名,不能包含路径穿越。图片文件名只允许常见图片扩展名,例如 jpg、png、webp、svg、avif 等。
图片上传也有两种方式:
- 传入 base64 编码内容。
- 传入本地图片路径。
如果使用本地路径,必须先配置一个允许读取的图片目录,并且图片真实路径必须位于这个目录内。这个限制很重要:AI 不能借上传图片的名义读取任意本地文件。
图片大小也有默认限制,必要时可以通过配置调整。
创建文章的流程#
create_post 是最常用的工具。它的流程是:
- 接收标题、正文、标签、关键词、摘要等参数。
- 如果没有传日期,就生成当前时间。
- 根据标题和日期生成文件名。
- 组装标准 frontmatter。
- 检查目标文件是否已经存在。
- 通过 Gitea API 创建文章文件。
- 写入清晰的提交信息。
这篇文章本身就是通过这个流程写入博客的。使用 MCP 创建文章的好处是,AI 不需要知道 Gitea API 的细节,也不需要手动处理 base64、sha、frontmatter 这些杂项。
更新文章的流程#
update_post 采用“先读再改”的方式。它先读取现有文章,拿到当前 frontmatter、正文和文件 sha,然后只覆盖用户传入的字段。这样修改摘要、标签、标题时,不会误删其他 frontmatter 字段。
Gitea 更新文件时必须带上当前 sha,这可以避免盲写覆盖。提交信息也会描述修改对象,后续从历史记录里比较容易看出变化。
搜索和统计#
list_posts 会读取文章目录下的 Markdown 文件,解析 frontmatter 后返回标题、日期、标签、摘要、草稿状态,并按日期倒序排列。
search_posts 在标题、摘要和正文里做简单关键词搜索,也支持标签和日期范围过滤。它不是全文搜索引擎,但对个人博客来说足够直接。AI 可以先用它找上下文,再决定是否读取完整文章。
list_tags 基于文章列表统计标签出现次数。这个工具适合在写新文章前检查已有标签,避免同一个概念出现多个近似标签。
为什么这样设计#
这个项目的实现思路比较克制:Hugo 继续做 Hugo 擅长的静态站点生成,Gitea 继续做仓库和部署触发,MCP 只负责把“读写博客”这件事包装成 AI 能调用的工具。
这种拆法有几个优点:
- 内容仍然是普通 Markdown 文件,可迁移性高。
- 构建和部署仍然走 Git 工作流,可追踪、可回滚。
- AI 权限集中在少量工具里,边界清楚。
- frontmatter、文件名、图片路径这些容易出错的细节被代码统一处理。
- 后续增加工具比较自然,例如草稿发布、文章改名、批量修标签等。
后续可以改进的地方#
当前版本已经能覆盖日常使用,但还有一些自然的扩展方向:
- 给 MCP 服务补测试,尤其是 frontmatter、路径校验、图片上传限制。
- 增加文章重命名工具,处理标题变化后文件名不一致的问题。
- 给
search_posts增加更好的摘要命中位置,而不是只截取正文前 150 个字符。 - 增加草稿发布工具,把
draft从 true 改为 false 并更新发布时间。 - 在部署流水线里增加 Hugo 构建产物检查,例如确认构建后的首页文件存在。
- 把 MCP 服务的配置示例单独整理成一份不含真实配置值的部署文档。
总结#
这个项目的核心不是“搭一个博客”这么简单,而是把博客变成一个可以被 AI 稳定操作的内容系统。Hugo 负责生成,Gitea 负责版本和部署,MCP 负责把写作、检索、更新、上传图片这些动作变成受控接口。
最终效果是:博客仍然保持静态站点的简单和可靠,但写作入口变得更接近对话式工作流。以后只要继续围绕 MCP 工具补能力,就可以在不破坏 Hugo 原有结构的前提下,让博客逐步变成一个更顺手的个人知识发布系统。