这个仓库表面上是一个很轻量的 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 服务自身的开发改动不必每次都触发站点发布。

流水线核心步骤如下:

  1. 拉取仓库。
  2. 使用 Hugo Docker 镜像构建静态站点。
  3. 将生成的 public/ 目录同步到远端部署目录。
  4. 成功或失败后发送部署结果通知。

构建阶段没有依赖 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 工具参数。
  • typescripttsx:开发与构建。

服务启动后通过 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。
  • 解析出 metabody
  • 重新生成 frontmatter 时保持统一格式。
  • 生成固定时区的 ISO 8601 时间。
  • 根据标题生成文件名。
  • 校验传入日期是否合法。

创建文章时,服务会自动补齐这些字段:

title
date
author
tags
keywords
description
showFullContent
readingTime
hideComments

这保证了 AI 写出来的文章不会因为漏 frontmatter 字段而破坏列表页显示。

路径与图片安全#

mcp/src/paths.ts 是安全边界之一。文章文件名必须是单个 .md 文件名,不能包含路径穿越。图片文件名只允许常见图片扩展名,例如 jpgpngwebpsvgavif 等。

图片上传也有两种方式:

  1. 传入 base64 编码内容。
  2. 传入本地图片路径。

如果使用本地路径,必须先配置一个允许读取的图片目录,并且图片真实路径必须位于这个目录内。这个限制很重要:AI 不能借上传图片的名义读取任意本地文件。

图片大小也有默认限制,必要时可以通过配置调整。

创建文章的流程#

create_post 是最常用的工具。它的流程是:

  1. 接收标题、正文、标签、关键词、摘要等参数。
  2. 如果没有传日期,就生成当前时间。
  3. 根据标题和日期生成文件名。
  4. 组装标准 frontmatter。
  5. 检查目标文件是否已经存在。
  6. 通过 Gitea API 创建文章文件。
  7. 写入清晰的提交信息。

这篇文章本身就是通过这个流程写入博客的。使用 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 原有结构的前提下,让博客逐步变成一个更顺手的个人知识发布系统。