Ease Ops - MCP 给 AI 一双操作 CI/CD 的手

2026/10/23 实践总结MCP

# 前言

给 AI 一双操作 CI/CD 的手 —— ease-ops MCP 介绍

上一篇讲了 ease ops - 自动化接入Skill 设计 (opens new window):AI 怎么"照着规程"把仓库接进平台。 这一篇讲规程底下的那双"手"——MCP 服务:26 个工具长什么样、怎么连怎么用、 以及它和 Skill 是怎么分工、怎么互相锁死的。

# MCP 是什么:USB-C 的比喻,和它没说的部分

MCP(Model Context Protocol)是 2024 年底 Anthropic 开源的一个协议,管一件事:AI 客户端(Cursor、Claude Code 等)怎么标准化地发现并调用外部系统的能力。你可以把它理解成 USB-C——以前每个 AI 工具要各家写各家插件,现在只要双方都"长这个口",插上就能用。

一个常见的误解是:接上之后 AI 就"什么都会了"。不是的。MCP 只解决通道问题,它规定了三件很窄的事:

  1. 发现:客户端问服务器"你有哪些工具?"(tools/list),服务器回一份清单,每个工具带名字、参数 schema、一段给模型看的描述;
  2. 调用:客户端发 tools/call,带上工具名和参数,服务器返回结果或错误;
  3. 元信息:工具可以带注解(是不是只读、是不是破坏性、能不能重复调),服务器可以带一段全局说明。

至于"AI 该怎么用这些工具把事情做对",协议一概不管。这正是留给我们要设计的东西——也是这篇后半段 Skill 存在的原因。

# ease-ops 的 MCP 长什么样

先看最小的事实。服务端用 Go + mark3labs/mcp-go 实现,挂在主服务的 HTTP 上(默认路径 /mcp,配置里 mcp.enabled 开关),传输方式是 Streamable HTTP:

Cursor / Claude Code ──POST /mcp──▶ ease-ops-api ──▶ 26 个工具 ──▶ 复用 biz 层用例
1

值得注意最后一节:MCP 没有自己的业务层。internal/mcp/deps.go 里注入的全是现成的 Usecase(Projects / Envs / Flows / Onboard……),和 web 控制台走的是同一套校验与权限。AI 调 ease_ops_apply_onboarding 和人点向导按钮,落到的是同一个函数——不存在"给 AI 开的后门接口"。

鉴权是一把 eops_ 前缀的 API Key,放在 HTTP 头里:

  • Key 与用户身份绑定,服务端只存哈希,明文只在创建时返回一次;
  • 每个工具调用先过中间件:ctx 里没有用户身份直接拒绝(authentication required);
  • 权限是项目级的:工具内部先解析项目、把该用户在该项目上的权限码写回 ctx,再 requirePerm。你在 web 上看不到某个项目,通过 MCP 一样看不到——AI 不获得任何额外权限。

26 个工具按"旅程"排序注册——身份 → 工作空间 → 项目 → 环境 → 流水线 → 构建 → 接入 → 凭证 → 密钥。tools/list 的顺序就是这条旅程,模型从上往下读,读到的一半是流程。

# 六个接口面设计,以及各自防的事故

MCP 服务器做得好坏,全在"接口面"上:模型看到的每个字节。下面六个决定,每个都对应一种真实会发生的事故。

# 1. 一段全局说明书,让 26 条工具描述各自只说一句话

server.go 里有一段 serverInstructions,随协议初始化下发给模型,把跨工具的共识集中讲一次:工作空间怎么定、引用怎么给、两种非失败返回、密钥边界。注释写得很直白:

它把跨工具的共识……集中讲一次,避免在 26 个工具描述里重复。

于是每条工具描述只写"做什么 + 用哪个工具接着做",测试里锁了上限——普通工具 120 字符(plan/apply 两个安全语义重的放宽到 400)。不这么做的话,每条描述都会膨胀成小作文,26 份重复的"先调 whoami"最后会把真正有用的 token 挤掉。

# 2. 返回形状只有三种,其中一种不是失败

envelopes.go 把工具返回值收敛成一个很反直觉的约定:

形状 含义 模型该做什么
正常 JSON,其中 status=need_input 缺上下文,不是失败 从 candidates 里挑一个,重试
{"error":{code,message,field,hint,fixable}} 结构化失败 fixable:false 就停下问人
普通 isError 消息 权限不足 / 参数非法 / 下游故障 绝不猜测式重试

need_input 是给 AI 专门设计的对话钩子。比如你只说"给项目加个环境变量",但你的 Key 能访问三个项目——服务器不猜,返回:

{ "status": "need_input", "need": "project",
  "hint": "these repos match several accessible projects; pass project to pick one.",
  "candidates": [ {"code":"shop","name":"商城"}, {"code":"payment","name":"支付"} ] }
1
2
3

歧义被原样上抛给模型裁决,而不是在服务端做静默选择。AI 最怕的不是报错,是"看起来成功了但其实选错了"——need_input 让每一步选择都显式。

# 3. 只收逻辑名,几乎不收 ID

全局说明里一句话:pass names or codes, never numeric ids。唯一的例外是 build_id,而且规则是"原样传回"(工具还专门容忍了模型把字符串 ID 回传成数字的情况)。

这条是防"AI 幻觉编 ID"的:模型想不起某个 ID 时,它会造一个看着合理的数字——如果接口收 ID,这个错误会一路穿透到数据层;如果接口只收名字,解析不过就返回 need_input,错误被拦在入口。ID 是平台内部的事,模型连犯错的语言都不具备。

# 4. 工作空间:服务器看不见你的终端,就让你钉一次

MCP over HTTP 有个先天缺陷:服务器不知道客户端的工作目录,"当前仓库"这个问题无解。ease_ops_use_workspace 的处理是让客户端自己上报:

你: git remote get-url origin
     → git@github.com:acme/shop.git
AI: use_workspace { repo: "git@github.com:acme/shop.git" }
     → { status: "ready", project: {code:"shop"}, components:[...] }
     (之后所有工具的 project 参数都可以省略)
1
2
3
4
5

绑定按用户记在服务端内存里(一人同时只"站在"一个仓库里,语义接近 shell 的 cwd),同时 AI 会把结果写进本地的 .ease-ops/workspace.json——下次会话直接读文件,连这个工具都能跳过。绑定失效(项目被删、权限被收回)时自动走 rebindWorkspace:只剩一个可访问项目就无缝接上,否则回 need_input。

# 5. 注解四件套:让客户端知道哪个工具危险

mcplib.NewTool 的默认注解是"破坏性、开放世界"——不显式覆盖的话,26 个工具在客户端看来个个都是危险操作,要么全弹确认框,要么用户学会了一路回车(后者更糟)。所以每个工具注册时必须显式声明四类工具身份(read / write / upsert / delete),漏写直接注册期 panic,测试 TestToolAnnotationValues 还锁了每个工具的具体取值。防的是一次手滑把"get_build 改成 writeTool"这种静默回归。

# 6. 密钥不过对话

协议层的约定只有一句:secret 永不经过对话。落到接口上是两个工具接力:

AI: create_secret_request { project:"shop", name:"prod/DB_PASSWORD", purpose:"env_variable" }
     → { url: "https://console.../intake/xxxx" }     ← 一次性填写页
你: (在浏览器里填,填完说"好了")
AI: get_secret_request { request_id }  → status: filled → 拿到 credential_ref 继续
1
2
3
4

AI 全程只知道"缺哪把密钥",从不知道值是什么——不是加密做得好,而是值的通道本来就不在 AI 的可达空间里。

# 怎么用:从一行配置到一段对话

第一步,拿 Key、配 MCP。 在 ease-ops 控制台创建 MCP API Key(只显示一次),然后写进客户端配置,以 Cursor 为例(.cursor/mcp.json):

{
  "mcpServers": {
    "ease-ops": {
      "url": "https://console.ease-ops.example.com/mcp",
      "headers": { "Authorization": "Bearer ${env:EASE_OPS_TOKEN}" }
    }
  }
}
1
2
3
4
5
6
7
8

推荐用环境变量插值(Cursor、VS Code 支持),Key 不进文件、配置可以放心提交;不支持插值的客户端只能写明文,那就要把文件权限收到 600 并加进 .gitignore。

第二步,直接说人话。 配好之后不需要学任何命令,工具清单和全局说明会自动进入模型的上下文。两类典型场景:

随手查询——不需要 Skill 教,模型顺着 instructions 里的推荐顺序就能走:

你:shop 项目 test 环境最近一次构建挂了,帮我看下为什么

AI:(whoami 确认身份 → use_workspace 钉项目 → list_builds 找到最近一次 → get_build {build_id, include:["steps","diagnosis"]}) 构建 #37 失败在 shop-api 的构建步骤,诊断显示 go.mod 里依赖版本冲突……点这里看流水线详情:links.pipeline_detail

注意最后那个链接:每个响应都内嵌 links 深链映射,AI 直接把 URL 给你,不许自己拼——对话里"AI 编出一个 404 链接"这类事就是这么防掉的。

接入仓库——这是唯一被 Skill 接管的重写链路(下一节讲为什么):

你:把这个仓库接进 ease-ops

AI:check_repo → 读本地文件 → 写计划 → plan_onboarding → 摘要给你确认 → apply_onboarding → 交接

排障三件事:连不上或 401,先查 Key 是否有效、配置里变量是否注入成功;工具列表为空,多半是服务器没开 mcp.enabled;AI 说"skill 版本过旧",那是下一节的版本门禁拦下了你,重跑安装器即可。

# MCP 与 Skill:能力和规程的关系

先给结论式的分工表:

MCP Skill
本体 服务端进程,26 个带 schema 的工具 一组 markdown 文件,装在客户端
回答的问题 "能调什么、怎么调" "该在什么时候调、调之前谁点头"
生效层 协议层:鉴权、校验、need_input 对话层:阶段、硬规则、话术
消费者 任何支持 MCP 的客户端 支持 skill 机制的客户端

# 为什么光有 MCP 不够

理论上,不装 Skill 模型也能看着 26 个工具自己编排——查询类场景确实没问题。但接入链路不一样:apply_onboarding 会建项目、改流水线,模型"发挥"的代价是真实的。缺了 Skill,会发生三件事:

  1. 模型可能跳过确认直接 apply——协议层没有任何机制禁止它这样做;
  2. 模型可能把密钥明文写进 payload——虽然服务端会以 secret_in_payload 硬错误拒绝,但它要多试错几轮才知道;
  3. 模型遇到 pre_build_blockers 可能自动重试 apply——而 apply 的部分失败恰恰最需要人来裁决。

所以 Skill 把这三件事写成了硬规则(没 plan_id 不 apply / secret 不进对话 / 确认必须显式)。反过来,Skill 也不只是"更详细的说明书"——看下一小节。

# 规则的两层实现:话说在前面,闸门在后面

Skill 和 MCP 对同一条安全约定的执行是两层的,这是整个设计里最值得展开的一点:

安全约定 Skill 层(提示词) MCP/服务端层(机制)
不许未确认就执行 "确认必须是显式的" apply 必须携带有效 plan_id,payload 指纹不匹配直接拒绝
不许带风险执行 逐条向用户过 acknowledge_required acknowledge 未回填 → ack_missing
不许碰密钥 "不写入文件、不复述" payload 里出现 values → secret_in_payload 硬错误
不许用内部 ID "对外只出现逻辑名" schema 只收名字/编码,未知字段严格报错

Skill 的价值是让模型根本不发起错误的调用(省一轮试错、少一分漂移);服务端的价值是即使模型没听劝,也做错不了。提示词不可靠是正常的,所以每一条规矩都必须有一个服务端闸门做后盾——只有提示词的规则是许愿,只有服务端规则的体验是灾难,两层都在才成立。

# 版本握手:skill 和 MCP 谁也不许跑在对方前面

whoami 返回 min_skill_version: 2.0.0,而 SKILL.md 头部写着 version: 2.0.0。Skill 的阶段 0 就是比对这两个值:服务端把工具面从 55 个重写成 26 个时,把下限抬到 2.0.0,所有装旧版 skill 的用户会在第一个工具调用就被拦下并提示"重跑安装器",而不是带着对旧工具名的理解一路猜下去。

# 一起演进:文档不许引用不存在的工具

最后一道保险在 CI 里。registry_test.go 有两个不显眼的测试:

  • TestToolSurface:工具面锁死为 26 个具体名字,多一个少一个都红——工具面对模型就是承诺,不能悄悄变;
  • TestSkillDocsOnlyReferenceRegisteredTools:扫描 skills/ease-ops-onboard/ 下全部 markdown(外加设计文档),凡是写了 ease_ops_* 的地方,都必须对应一个已注册的工具,否则测试失败。

也就是说:SKILL.md 里写了一个不存在的工具名,是编译不过去的。文档和代码不是靠人肉同步,是靠测试焊死。这就是"skill 是 MCP 的说明书"这类说法的升级版——说明书被钉在了服务器身上,撕一页都留痕迹。

# 结语:双层的 Agent 接口

把两篇合起来看,ease-ops 给 AI 暴露的其实是一个双层结构:

Skill(对话层)   五阶段 · 三条硬规则 · references 按需加载   ← 规程
MCP(协议层)     26 工具 · need_input · 逻辑名 · 密钥绕行     ← 能力 + 闸门
biz(业务层)     与 web 向导共用的同一套用例与权限             ← 事实
1
2
3

查询类轻量场景,模型沿 MCP 的全局说明自由发挥就够了;接入这种重写场景,才值得上 Skill。判断标准就一条:AI 自由发挥出错的代价,你付得起吗?

而不管轻还是重,MCP 这一侧的纪律是共通的:把歧义上抛(need_input)、把幻觉堵死(只收逻辑名)、把密钥绕出对话(intake 页面)、把危险显式标注(注解四件套)——不是让 AI 更强,而是让它能犯的错误更少。