Typecho MCP Workbench 开发手记 02:SSH 零操作,把远程博客接成本地工作台

上一篇写的是这个项目为什么存在:我不想再做一个“更漂亮的 Typecho 后台”,而是想把远程博客变成一个本地可控、AI Agent 可操作、远端尽量轻量的发布目标。

这篇继续往下走,讲最关键的一段链路:SSH zero-touch deployment

所谓 zero-touch,不是说系统里没有部署动作,而是说远端不需要人手工登录后台、安装插件、改配置、开公网接口。用户给本地工作台一个 SSH 目标,本地侧自己完成探测、部署、健康检查和后续调用。远端 Typecho 仍然是原来的 Typecho,只是多了一个很小、可审计、可移除的 PHP agent。

SSH zero-touch deployment:本地工作台通过 SSH 接入远程 Docker Typecho

为什么不用 Typecho 插件起步

一开始很容易想到“写一个 Typecho 插件,暴露 JSON API”。这条路并不奇怪,甚至很自然:Typecho 是 PHP 应用,插件机制也在那里。

但它有几个我不太喜欢的前提:

  • 用户要先登录后台安装插件。
  • 远端要新增一个公网可访问的 API 面。
  • 鉴权、限流、日志、撤销策略都要在博客服务器上承担。
  • 如果 AI Agent 误操作,远端插件会直接站在第一现场。

对于一个面向写作和发布的 MCP Workbench,我更想要的边界是:MCP 留在本地,远端只做被调用的最小执行器。AI Agent 不直接碰服务器,不直接连数据库,也不拿到一个公网管理 API。它只调用本地 MCP 工具;本地工具再走统一的策略、快照、审计和 SSH 调用链。

所以当前方案变成了:

AI Agent / 人类工作台
  -> 本地 MCP / local-service
  -> SSH
  -> 远端 PHP agent
  -> Typecho 文件和数据库

这条链路看起来绕了一点,但每一层都有清楚的职责:本地负责控制和安全,SSH 负责传输,PHP agent 负责很窄的 Typecho 操作。

第一步:先找到 Typecho 在哪里

zero-touch 的第一件事不是上传文件,而是探测。

很多 Typecho 博客不是标准的一台机器一个目录。有的人放在 /www/wwwroot,有的人放在 public_html,也有人用 Docker、Dockge 或别的面板把 Typecho 跑在容器里。这个项目当前优先支持的真实场景就是 Docker 里的 Typecho:本地通过 SSH 进宿主机,再找到能够执行 PHP 的容器。

探测逻辑不会全盘扫服务器,而是看一组高价值线索:

config.inc.php
index.php
var/Typecho/
var/Widget/
usr/themes/
usr/plugins/

这些标记组合起来,比单独看一个 index.php 靠谱得多。探测结果会带上候选根目录、置信度、运行模式和可用执行方式。如果只有一个高置信候选,本地可以自动选择;如果有多个候选,未来工作台应该让用户确认。

这个设计的重点是克制:探测只回答“这里是不是一个 Typecho,以及应该怎样执行它”,不顺手做写入,不修改站点,也不猜测业务配置。

SSH zero-touch 从连接、探测、识别 Docker 到部署 agent 的流程图

第二步:部署一个很轻的 PHP agent

找到 Typecho 之后,本地会准备远端 runtime。理想位置是 Typecho 根目录下的:

.typecho-mcp/

如果站点目录不可写,后续也可以退到用户 home 目录下的项目私有路径。无论放在哪里,原则都一样:只写项目自己的目录,只删除项目自己的目录,不碰主题、插件、上传文件和 Typecho 核心文件。

PHP agent 本身也保持小。它不是一个常驻服务,不监听端口,不对公网开放。它只在本地通过 SSH 触发时运行一次,从 stdin 读 JSON,请求完成后把 JSON 结果写到 stdout,日志走 stderr。

这条约束很重要:

stdout 只能是协议 JSON
stderr 才能放日志

因为本地侧要把远端执行看成一个可组合的函数调用。如果 stdout 里混入 warning、debug 或 HTML,协议就会变脏。把协议输出和日志输出分开,后面做 MCP 返回、审计、错误分级都会轻松很多。

stdin 和 stdout 协议分离:本地 MCP Server 调用远端 PHP agent

第三步:通过 SSH/stdin/stdout 调用

Docker 场景下,最终调用大致长这样:

ssh sg1 "docker exec -i typecho-php-1 php /app/.typecho-mcp/current/agent.php"

本地把请求写进 stdin:

{
  "jsonrpc": "2.0",
  "id": "op_xxx",
  "method": "posts.get",
  "params": {
    "cid": 87
  }
}

远端把结果写回 stdout:

{
  "jsonrpc": "2.0",
  "id": "op_xxx",
  "result": {
    "post": {
      "cid": 87,
      "title": "Typecho MCP Workbench 开发手记 01..."
    }
  }
}

这不是让远端 PHP agent 变成 MCP Server。MCP Server 仍然在本地,负责给 AI Agent 暴露工具;远端 agent 只是本地服务通过 SSH 调用的执行器。

这个分工让我比较安心:AI Agent 看到的是 typecho.posts.create_drafttypecho.posts.prepare_publishtypecho.posts.publish 这样的本地工具,而不是一把 SSH key 和一个数据库路径。

第四步:把写操作收束到本地策略里

真正危险的不是“能不能连上服务器”,而是“连上之后谁有权做什么”。

所以项目里所有写操作都要走 local-ops:

  • 创建草稿可以直接执行,但要写审计日志。
  • 更新文章前要先做本地快照。
  • 发布文章前要经过 publish policy。
  • 回滚、删除、评论、插件启停这类高风险动作必须显式确认。

这次第 2 篇文章的发布也走了同一条路径:MCP 工具创建草稿,MCP 工具做发布前检查,最后带 confirm=true 和明确的 client 标识发布。也就是说,连这个系列本身也在用它正在讲的系统发布。

这种自举很有意思。它会逼项目面对真实问题:分类和标签能不能写进去?图片能不能走 Typecho 媒体路径?发布前检查能不能发现空 slug、空分类、失效图片?发布后能不能回读验证?

如果这些路径在写项目文章时不顺手,那它也很难真正服务日常写作。

第五步:远端轻,未来才好扩展

SSH zero-touch 还有一个好处:它给后续客户端留出了空间。

今天的入口是本地 Web workbench 和 MCP stdio;以后可以是桌面应用、菜单栏工具、移动端审核器,甚至是多个 AI Agent 的协作工作流。但这些入口都不应该各自发明一套远端写入方式。它们应该共享同一个本地服务、同一套策略、同一份快照和审计记录。

远端 PHP agent 越轻,本地控制面就越容易演进。要加缓存、请求日志、发布报告、图床策略、回滚预览,都可以在本地层完成。远端只需要稳定地执行少数 Typecho 原子操作。

这也是我现在越来越喜欢这个架构的地方:它没有试图把 Typecho 改造成一个现代 API 平台,而是尊重它已经是一个成熟博客系统,然后在本地给它接上一层适合 AI 协作的控制面。

这一篇完成后的状态

到这里,项目已经从“有一个想法”走到了“远程博客可以被本地 MCP Workbench 操作”:

  • SSH 可以发现 Docker/Typecho 执行目标。
  • 远端 PHP agent 可以通过 stdin/stdout 执行受控方法。
  • 本地 MCP Server 可以把这些能力包装成 AI Agent 工具。
  • 草稿、媒体、预发布检查和发布都走项目自己的路径。
  • 发布动作有策略、快照和审计记录,而不是直接写数据库。

下一篇会写 MCP 工具层本身:为什么工具名和参数要设计得像编辑流程,而不是像数据库函数;以及 AI Agent 创建草稿、更新文章、准备发布报告、发布和回滚时,哪些约束必须放在工具边界里。