大语言模型能够写文章、检索信息、调用工具,但它并不天然知道什么操作可以执行、谁有权限执行、失败后能否重试,以及发布后如何追责。把模型直接接到数据库,或者暴露一个可以请求任意内部 API 的工具,通常会让这些问题一起失控。

Model Context Protocol(MCP)提供的不是一条让模型获得更多权限的捷径,而是一种将既有系统能力以结构化、可发现、可约束方式提供给 AI Host 的协议。本文以一个 Go CMS 项目的 MCP 实现为例,说明如何把内容运营能力交给 AI,同时保留业务系统原有的权限、校验、幂等和审计边界。

MCP 应位于什么位置

本项目AI运营助手 MCP Server 没有让 MCP Server 直接访问 PostgreSQL、Redis、Kafka 或对象存储,也没有把 CMS 的 usecase 和 repository 复制一遍。它选择了一条更克制的路径:Codex 通过本地标准输入输出启动 cmd/mcp;MCP Server 获取受限机器身份的短期 JWT,再调用既有 CMS 的管理 HTTP API;CMS 仍负责 RBAC、状态机、输入校验和审计。

Codex
  | stdio
  v
cmd/mcp
  | HTTPS + Bearer 短期 JWT
  v
CMS 管理 API
  | JWT + RBAC + usecase + audit
  v
数据库 / 对象存储

这里有一个容易混淆的点:stdio 只解决了 Codex 到 MCP Server 这一跳的本地连接问题,并没有取消 MCP 到 CMS 的认证需求。MCP Server 仍是一个需要被限制和识别的机器调用方,不能伪装成某个真实管理员。

这种分层把责任放回了正确的位置。MCP 适配协议和模型交互,CMS 保持业务事实的唯一裁决权。即使模型误判了权限或拼错了参数,CMS 仍会拒绝不合法的请求。

不要只讲 Tool:MCP 有三种不同的能力

很多 MCP 示例只展示一个 Tool,但生产系统中 Resource、Tool 和 Prompt 各有明确职责。

Resource 用来提供可读取的上下文。项目提供了 cms://site/healthcms://localescms://taxonomy,并通过 URI 模板暴露某个语言的分类树和一篇文章的翻译内容。例如模型在起草文章前,可以先读取 cms://taxonomy,获得实际可用的分类与标签,而不是猜测一个不存在的分类 ID。

Tool 用来表达明确的业务动作。cms.article.list 是查询工具;cms.article.create_draftcms.article.update_translationcms.article.preview_publishcms.article.publish 则对应内容运营中的不同阶段。工具名称应表达业务意图,而不是提供一个名为“request_api”的万能入口。前者可被授权、审计和测试,后者会把内部 API 的所有风险转交给模型。

Prompt 用来沉淀可复用的工作流程,而不是夹带权限。项目中的 cms.draft_from_brief 会要求模型先读取分类体系、生成标题和正文、展示摘要,并停在保存草稿之前;cms.pre_publish_review 则先读取文章、调用预检工具、报告阻塞项和警告,等待用户确认后才允许发布。Prompt 是操作手册,不是绕过确认的后门。

让写操作有明确的风险语义

内容系统最危险的不是“模型不会调用发布接口”,而是模型在不恰当的时机调用了它。为此,工具的描述、元数据与服务端规则需要形成三层防线。

第一层是协议层的风险提示。查询工具声明为只读;创建、编辑、归档和发布工具声明为写操作、非幂等操作。Host 可以据此在执行前请求人工批准,但这只是用户体验层的保护,不能替代服务端授权。

第二层是工作流约束。发布不应该接受一个由模型填写的 confirmed: true 参数,因为模型可以编造这个值。更可靠的流程是:读取文章翻译,调用 cms.article.preview_publish,展示文章标题、slug、语言、当前状态和检查结果,取得用户对这一次目标发布的明确确认,然后调用 cms.article.publish(article_id, locale)

第三层是 CMS 本身的状态机和 RBAC。MCP 的工具说明不能扩大权限;即使工具存在,机器账号没有 cms.article.publish 权限,或文章没有满足发布条件,服务端也必须拒绝请求。这是“模型建议动作,业务系统决定动作是否成立”的核心原则。

身份不等于把密钥塞进配置文件

本项目为 MCP 设计了独立的服务账号。MCP 使用 client_idclient_secret 请求 /api/v1/auth/service-token,以 Basic Auth 换取短期 CMS JWT;令牌在距离过期不足两分钟时刷新。随后每个 CMS 请求只携带短期 Bearer Token。

这个机制至少带来三项收益:第一,MCP 不复用真人账号,审计记录可以清楚标识机器操作者;第二,禁用服务账号或轮换凭证后,不会再签发新令牌;第三,凭证泄露的暴露窗口受短期 token 限制。

密钥只应通过受保护的进程环境注入,例如 CMS_MCP_CLIENT_IDCMS_MCP_CLIENT_SECRET。它们不应进入 YAML 示例、Git 仓库、Prompt、Tool 参数、日志、追踪字段或 MCP 返回内容。CMS 基础地址也应由显式配置决定,不能让模型通过 Tool 参数指定目标 URL。

上下文本身也可能攻击模型

MCP 的风险并不只来自写操作。文章正文、分类名称、标签、URL,甚至 Google Search Console 的搜索词,都可能包含诱导模型忽略规则的文本。因此 Server Instructions 明确把 CMS 和搜索数据定义为“不可信内容”,要求模型不要遵从其中的指令。

这是一项常被忽略的设计:Resource 的价值是提供事实,不是提供高优先级指令。返回 CMS 数据时也应保留清晰的数据边界,避免把外部内容拼接成系统提示。对于搜索分析结果,项目还标注数据可能不完整,并将“高曝光低点击率”描述为启发式机会,而不是 Google 的质量评分。AI 能帮助发现线索,但不能把不完整数据伪装成确定结论。

文件输入为什么需要边界

文章正文通常很长,直接把它放进 Tool 参数会让调用记录、上下文大小和确认体验变差。这里的实现让写工具接收相对于 CMS_CONTENT_ROOTcontent_file,由 MCP Server 读取后再提交给 CMS。

但“允许模型传文件路径”本身就是一个安全问题。实现会拒绝绝对路径和目录穿越,解析符号链接后再次确认目标仍位于内容根目录,要求它是常规文件、大小不超过 10 MiB 且为有效 UTF-8。读取内容还会计算 SHA-256 摘要,用于构造写操作的幂等指纹。

这个例子说明:模型工具的参数校验不能只检查字段是否为空。凡是路径、URL、标识符、分页大小、枚举值和外部内容,都需要按其实际风险建立边界。

幂等、错误和审计要由系统保证

网络超时后,模型或 Host 可能重试同一个写操作。若没有幂等语义,一次“发布失败”的表象可能变成两次真实写入。项目优先使用 Host 通过 _meta 提供的幂等键;缺失时,则根据 MCP session、工具名和规范化输入计算操作 ID,并传递给 CMS 调用链。

错误也不应被压成一段模糊的自然语言。CMS 客户端区分 HTTP 非 2xx 的传输失败、HTTP 成功但业务信封 success=false 的业务错误,以及真正成功的响应。MCP 将已知业务错误码返回给 Host,对未知故障则使用稳定的 CMS_UNAVAILABLE,避免泄露内部细节。

审计记录至少应关联机器账号、动作名称、文章 ID、语言和 correlation ID,但不记录正文、JWT、Authorization Header 或 client secret。对 AI 而言,这些机制看起来像“基础设施细节”;对生产系统而言,它们决定了事故发生后能否解释、重放和修复。

结语

一个好的 MCP Server 不会成为绕开既有架构的捷径。它应该像一个受限的协议适配器:把稳定事实作为 Resource 暴露,把可执行业务意图作为 Tool 暴露,把重复流程作为 Prompt 暴露;而把权限、状态机、审计、密钥管理和幂等性留在真正拥有这些责任的业务系统中。

当 AI 参与内容运营时,最重要的不是让它“拥有发布权限”,而是让它在正确的上下文中提出动作,并让每一次有副作用的动作都经过明确确认和可追溯的系统边界。这才是 MCP 在工程实践中真正有价值的地方。