写博客时,真正麻烦的往往不是把文章写出来,而是写完以后那一串重复动作:打开后台、粘贴 Markdown、调整分类和标签、上传图片、检查发布结果。博客从 Typecho/WordPress 迁到 Hugo 后,这个后台没有了,文章只剩下 content/posts 里的 Markdown 文件。
那能不能让 Codex 直接管理 Hugo?可以,但我不想把服务器 shell 权限交给 AI,也不想为了一个个人博客再装一套 CMS。最后采用的方案,是在 Hugo 外面加一个很薄的 MCP 服务:AI 调用的是受限的文章工具,服务负责校验、写文件、备份、审计和构建。
这篇记录的是实际部署方案,不是另起一个后台系统。
它能做什么
Hugo MCP 提供 JSON-RPC 2.0 接口,当前主要分四类工具:
- 读取:
hugo_list_posts、hugo_get_post - 文章:
hugo_create_draft、hugo_update_draft、hugo_publish_post、hugo_unpublish_post、hugo_delete_post - 媒体:
hugo_upload_media、hugo_list_media、hugo_delete_media - 运维:
hugo_migrate_post_bundle、hugo_build
新文章默认创建成 Page Bundle:
|
|
旧文章的 content/posts/<slug>.md 也可以继续读取和更新,需要时再通过迁移工具转成 Page Bundle。
它不提供 CMS 后台、评论管理、用户系统、全文搜索,也不负责 Git 提交。Hugo 还是展示层,Markdown 还是内容源,MCP 只是给 AI 一个有边界的写入口。
为什么直接做成 MCP
参考 Typecho AgentBridge 的思路,MCP 比单独写一个“AI 发布脚本”更适合这个场景:客户端可以发现工具和参数,调用过程有统一的 JSON-RPC 结构,也可以把权限、鉴权和并发保护集中放在服务端。
但 Hugo 和 Typecho 的边界不一样。Typecho 有数据库和后台用户;Hugo 没有数据库,文章就是文件。因此 Hugo MCP 不模拟一个虚假的 CMS,而是直接围绕文件系统做限制:只允许访问文章目录,只接受受控的 slug,只在需要时调用 Hugo 构建。
实际链路如下:
|
|
AI 得到的是“文章工具”,不是服务器终端,也不需要知道 SSH 私钥。
第一步:按 Docker 目录规范部署
这套服务放在 docker03 上,Compose 文件和运行数据分开:
| 内容 | 路径 | 作用 |
|---|---|---|
| Compose 文件 | /data/docker/service/hugo-mcp/ |
docker-compose.yml、构建上下文 |
| Hugo 站点 | /data/docker/appdata/hugo/data/site/ |
hugo.toml、content/、public/ |
| MCP 运行数据 | /data/docker/appdata/hugo-mcp/ |
token、备份、审计、幂等记录、trash |
MCP 数据目录不能放进博客 Git 仓库。尤其是 token、审计日志和回收站,不能因为同步文章而一起提交。
Compose 的关键挂载类似这样:
|
|
8095 只绑定本机回环地址。外部客户端通过 HTTPS 反向代理访问,不能把 MCP 端口直接丢到公网。
第二步:配置 Token 和入口
服务首次启动会在运行数据目录生成随机 token。token 只保存在服务器上,客户端使用环境变量注入:
|
|
实际公开入口是博客域名下的兼容路径,反向代理再把请求转给容器。没有 Bearer Token 的请求直接返回 401;服务不提供后台登录页,也不保存 GitHub 凭据。
连接后先执行 initialize,再执行 tools/list。客户端应该以服务端返回的工具合同为准,不要把工具参数写死在提示词里。
第三步:跑通一次完整写作流程
一次发布不是直接调用“发布”按钮,而是固定走下面的顺序:
|
|
创建草稿的 JSON-RPC 请求:
|
|
调用返回文章 revision。发布时把这次返回的 revision 原样传给 hugo_publish_post,服务构建完成后返回新的 revision。
修改已发布文章:revision 不能省
每篇文章的 revision 是当前 Markdown 文件的 SHA-256。所有会改变文章的工具都要求 expected_revision:
|
|
如果期间有人在本地编辑过文件,revision 不匹配,服务会拒绝写入。正确处理方式是重新读取文章,再根据新内容提交;不要绕过检查。
这个保护解决的是个人博客里很容易遇到的情况:电脑上的编辑器、Codex 和服务器上的同步任务同时碰了一篇文章。没有 revision 的话,最后一次写入会静默覆盖前面的修改。
删除、恢复和重复请求
写操作还有两个保护:
- 每次变更前先生成备份,删除文章时移动到私有 trash,不直接销毁。
- 每次写操作必须带唯一
request_id,结果会保存到幂等记录。网络超时后可以用同一个 ID 重试,不会重复创建或重复删除。
审计日志使用 JSONL 保存调用时间、工具名、request_id、slug 和结果。它不是完整的数据库事务,但足够用来判断一次超时请求到底有没有落盘。
图片、反向代理和几个坑
媒体上传不接受调用者本机路径,也不抓取远程 URL,而是通过 Base64 传给 MCP。当前只允许 JPEG、PNG、WebP、GIF,并检查文件签名;默认单文件上限为 5 MiB,请求体上限为 8 MiB。
几个实际要注意的点:
- Hugo MCP 不负责 CDN,也不会替你压缩图片。
- 公开入口必须走 HTTPS,8095 不应该直接暴露。
- 反向代理需要完整转发 POST、Authorization 和响应体。
- 同一个实例内有写锁;多副本部署还需要外部锁和共享运行数据,不能简单复制容器。
- taxonomy 管理不是当前工具的一部分,标签整理仍然是内容维护动作。
GitHub 放在哪一层
GitHub 不属于 Hugo MCP 的运行时依赖。博客内容维护在私有仓库,Hugo MCP 单独开源;需要提交源码时,由客户端调用 GitHub MCP 或 GitHub Actions 完成。
这样做把两种权限拆开了:MCP token 只能操作博客内容,GitHub Token 只交给 GitHub 侧的同步流程。MCP 服务本身不保存 GitHub 凭据,也不会因为 GitHub 暂时不可用而阻塞本地文章编辑。
测试范围与后续
当前已经验证了读取、创建草稿、更新、发布、撤回、软删除、Page Bundle 媒体和 Hugo 构建流程,也专门测试了 revision 冲突、重复 request_id、非法路径和未授权请求。
当前版本明确不做 CMS 后台、评论、全文搜索、Git 提交和多实例共享锁。对于个人博客,这个边界反而比较舒服:Hugo 继续保持简单,GitHub 继续保存内容历史,MCP 只负责把 AI 的动作限制在文章操作里。
项目源码、工具合同和部署说明:
这套方案的核心不是“让 AI 拥有后台权限”,而是把博客后台拆掉以后,重新提供一条可审计、可回滚、能做并发检查的内容写入路径。