如果今天有人问我该选哪个,我会给下面三条建议,依据是后面的实测。
- 工具已经有命令行、Agent 又能跑 shell 的时候,写一份 SKILL.md 比包一个 MCP server 省事,而且够用。 "省事"指实现和维护成本:一份 Markdown 对一个要长期维护的 server。运行成本上实测 MCP 最低;Skills 省掉的是裸 CLI 每次都要花的两轮"读
--help",最终上下文也是三者里最小的。 - 需要跨宿主复用、需要参数 schema 校验、宿主根本没有 shell 的时候,用 MCP。 它是三者里模型调用最少、耗时最短的,代价是你得写并维护一个 server;另外我用的 Python SDK 默认会吞掉工具异常的原因文本(后面有细节)。
- WebMCP 不是这三者的替代品。 它只存在于浏览器里、只在页面打开时存在,只有通过浏览器提供的接口访问页面工具的 Agent 才能调用它。它解决的是"网站怎么把自己的功能交给用户浏览器里的 Agent",和"你的 Agent 怎么接后端工具"是两个问题。
这四个词在 2026 年 9 月同时出现在会议议程上(本周的 AGNTCon + MCPCon Europe 有一场就叫 "Three Doors to One Tool: MCP vs WebMCP vs CLI")。这篇用同一个任务把前三者的账算一遍,第四个说明为什么没法放进同一张表。
实验:同一个任务,三种接法
任务:一个 SQLite 里的工单表,8 条工单。要求 Agent 关闭所有"分配给 kim、状态 open、超过 30 天没更新"的工单,评论固定为 Closed for inactivity,其他工单一律不许动。正确答案是关闭 101、102、107 三条。
表里埋了四个陷阱:一条 kim 的工单 23 天前更新过(不到 30 天,不能关);一条 kim 的已经是 closed;一条分配给 kimberly(前缀匹配会误伤);一条分配给别人但同样很旧。任务完成后由脚本把整张表和预期终态逐行、逐字段对比,多一行、少一行或任何字段不同都算失败。
三种接法,操作的是同一个数据库、同一套函数:
| 接法 | 模型看到什么 | 模型怎么执行 |
|---|---|---|
| MCP | 三个工具的 JSON Schema(list_issues_tool、get_issue_tool、close_issue_tool),由 Python mcp SDK v2 的 MCPServer 生成,走 stdio | 每个工具调用由测试台转发给 MCP server |
| CLI | 一个 bash 工具,加一句"系统里装了 issues 命令" | 模型自己跑 issues --help 摸索用法 |
| Skills | 同一个 bash 工具,加一份 250 字的 SKILL.md 放进 system prompt | 不用摸索,按 SKILL.md 里的命令和规则做 |
SKILL.md 按 Agent Skills 规范写,正文只有三条命令说明和三条规则:用 --json、用 --assignee 和 --state 在服务端过滤、逐条关闭并读返回。
度量:每次运行记录模型调用次数、工具调用次数、工具报错次数、累计输入/输出 token、最后一次调用的输入 token(即任务结束时的上下文占用)、静态占用(system prompt 加工具定义在第一次调用前的 token 数,用 count_tokens 算)、墙钟时间、数据库终态是否正确。
模型是 Claude Opus 5,默认思考设置,每种接法跑 5 次,共 15 次,总花费约一美元。整个测试台是一个 Python 文件,含种子数据、CLI、MCP server、SKILL.md 和运行器:tool_surface_comparison.py。
结果
15 次全部做对。差别全在过程上:
| 接法 | 正确 | 模型调用 | 工具调用 | 输入 token | 输出 token | 结束时上下文 | 静态占用 | 耗时 |
|---|---|---|---|---|---|---|---|---|
| MCP | 5/5 | 3.2 | 4.6 | 4,715 | 868 | 2,195 | 745 | 15.9 s |
| CLI | 5/5 | 7.4 | 6.8 | 15,680 | 1,450 | 3,623 | 415 | 25.7 s |
| Skills | 5/5 | 6.0 | 5.0 | 8,752 | 905 | 1,976 | 714 | 18.3 s |
(5 次均值。token 是 5 次累计输入/输出的平均。这是换用全表校验器之后重跑的一轮;之前用只查部分字段的校验器跑过一轮,均值为 MCP 3.2 / CLI 6.4 / Skills 6.0,同样 15/15 正确,两轮 CLI 的差异来自这一轮里一次 10 调用的离群运行,见后面的失败方式。)
三种接法的典型执行轨迹,取各自的第 2 次运行:
MCP(3 次模型调用):
list_issues_tool({"assignee": "kim", "state": "open"})
close_issue_tool({"issue_id": 101, "comment": "Closed for inactivity"})
close_issue_tool({"issue_id": 102, "comment": "Closed for inactivity"})
close_issue_tool({"issue_id": 107, "comment": "Closed for inactivity"})
第一次调用列出工单,第二次调用一口气发出三个关闭请求(并行工具调用),第三次调用写总结。
CLI(7 次模型调用):
which issues; issues --help 2>&1 | head -50
issues list --help; echo ---; issues show --help; echo ---; issues close --help
issues list --json
issues list --assignee kim --state open --json
issues close 101 --comment 'Closed for inactivity'; issues close 102 ...; issues close 107 ...
issues list --json
前两次调用全在摸索用法。第三、四次列表(先全量、再加过滤),第五次用一条 shell 命令关掉三条,第六次回头验证。
Skills(6 次模型调用):
issues list --json --assignee kim --state open
issues close 101 --comment "Closed for inactivity"
issues close 102 --comment "Closed for inactivity"
issues close 107 --comment "Closed for inactivity"
issues list --json --assignee kim --state open
没有摸索,第一条命令就带上了过滤参数。但三条关闭命令分了三次调用,因为 SKILL.md 第三条规则写的是"逐条关闭并读返回"。
逐项解读
调用次数:发现成本是最大的一笔
裸 CLI 比 MCP 多出的 4.2 次模型调用,有 2 次固定花在 --help 上,5 次运行次次如此;其余来自多列一次表、写脚本算日期这类防御动作(见后面的失败方式)。这是"模型不知道工具怎么用"的直接价格:每个新会话都要重付一次。
MCP 把这笔钱预付了:工具 schema 在第一次调用就在上下文里,模型直接选参数。Skills 也预付了,方式是把用法写成文字放进 system prompt。两者差别在于,MCP 的 schema 是机器生成、机器校验的;SKILL.md 是人写的,写得好坏直接决定模型的行为。
Skills 的 6 次调用里有 2 次是被我自己的规则逼出来的。"逐条关闭"让模型把本可以一条 shell 命令完成的事拆成三次往返。把规则改成"可以在一条命令里关闭多条",预计能减少往返,具体幅度我没有测。在这个任务上,"逐条确认"这条规则多花了两次往返;写 SKILL.md 时,每条约束都要想一下它会不会增加往返。
上下文占用:静态和动态要分开看
静态占用 CLI 最小(415 token),只有一个 bash 工具的定义。但结束时它的上下文最大(3,623),因为两轮 --help 的输出都留在了对话里,而且这些输出对后续每一次调用都要重新计费。
MCP 静态 745,三个工具的 schema 只比单个 bash 工具的定义多出约 330 token;结束时 2,195。Skills 静态 714,结束时 1,976,三者最小,因为它既不需要 --help 输出,工具返回也是紧凑的 JSON。
这个实验只有 3 个工具。工具到几十上百个时,MCP 的静态占用会线性增长,而 Skills 天然分层:Agent Skills 规范里 name 加 description 常驻(约 100 token 一个),正文只在激活时加载。MCP 这边,官方 8 月 22 日的路线图把"渐进式工具发现"列为优先方向,但截至 2026-07-28 规范它还是规划,不是已发布能力。今天要在 MCP 上做分层,得靠宿主侧的延迟加载,方法见第 38 章"Deferred Tool Loading 与 Tool Search"。
耗时:这次实验的时间主要花在模型往返上
15.9 秒、25.7 秒、18.3 秒,排序和模型调用次数一致。单次工具执行在这个实验里是毫秒级,时间几乎都花在模型往返上。想快,就减少往返;减少往返的办法是让模型第一次就知道该怎么调,或者让它一次调多个。
失败方式:没有一次失败,但方差不一样
15 次都对,所以这个任务不足以区分"谁更可靠"。但方差里有信息:
- CLI 的第 4 次运行用了 10 次调用、35,523 输入 token(其他四次在 8,800 到 11,500 之间)。模型先写了一段 Python 算日期差,接着
cd /tmp再执行关闭命令。测试台的 CLI 用相对路径找数据库文件,于是它在/tmp下新建了一个空库,关闭"成功"却什么都没改。模型察觉后grep了测试台的源码找数据库路径,回到正确目录重做三次关闭,最后删掉/tmp里那个空库。终态是对的,但过程里它读了和任务无关的代码,在工作目录外留下又清理了文件。裸 shell 给的自由,模型会用来做防御和收拾残局,代价是你的 token。相对路径这种 CLI 的老问题在 MCP 模式下不存在(工具在固定进程里执行);Skills 模式同样有 shell,这 5 次里没出现,但没有机制阻止。第 5 次运行也写了 Python 算日期,只是没走错目录。 - MCP 的第 5 次运行在关闭之前逐个
get_issue_tool复核了三条工单,多花 1 次调用、约 2,400 token。schema 里有这个工具,模型就会用它。 - Skills 的 5 次运行轨迹几乎一样,方差最小。规则写进 prompt 之后,模型的行为被约束住了。
还有一个在搭测试台时撞到的失败方式,和模型无关,和 SDK 有关。用 Python mcp SDK v2 的 MCPServer 写工具时,工具函数里 raise ValueError("issue 104 is already closed"),模型收到的是:
Error executing tool close_issue_tool
具体原因被吞了。要让模型看到原因,必须抛 SDK 自己的 ToolError。裸 CLI 没有这个问题:stderr 和退出码原样进上下文。这是我测的 Python mcp SDK v2 MCPServer 的默认行为,不是协议本身的规定,换 SDK 或版本可能不同。取舍在于:SDK 帮你规范了错误的形状,代价是你要按它的方式报错,否则模型会在"不知道为什么失败"的状态下重试。
WebMCP 为什么不在表里
WebMCP 是 Google 和 Microsoft 在 W3C 社区组里推动的浏览器标准草案,还没进入正式标准轨道。Chrome 从 149 版开始跑 origin trial,本地开发可以用 chrome://flags/#enable-webmcp-testing 打开。它做的事是让网页通过 document.modelContext.registerTool() 注册工具(字段和 MCP 一样:name、description、inputSchema、execute,外加 readOnlyHint 之类的注解),或者给一个 <form> 加上 toolname、tooldescription 这类属性,让浏览器自动把表单变成工具。
和 MCP 的三个根本差别,Chrome 的文档写得很直接:
| MCP | WebMCP | |
|---|---|---|
| 工具在哪运行 | 你的后端进程 | 用户浏览器里的页面 |
| 谁能调用 | 任何 MCP client | 通过浏览器接口访问页面工具的 Agent(内置、扩展或页内嵌入) |
| 活多久 | 进程活多久它活多久 | 页面关了就没了 |
| 身份和会话 | 你自己做 OAuth | 直接用页面已登录的 session 和 cookie |
所以它没法进这个实验:我的测试台是一个 Python 进程里的 Agent,没有浏览器,也没有页面。要测 WebMCP 得在 Chrome 里跑一个页面 Agent,那是另一个实验,测的也是另一个问题:"网站主"该不该给自己的页面装 WebMCP,而不是"Agent 开发者"该怎么接工具。
如果你两边都是,Chrome 文档的建议是并用:核心业务逻辑和后台任务走 MCP,用户正在页面上时的交互走 WebMCP。
选型表
| 场景 | 选 | 原因 |
|---|---|---|
| 工具已有 CLI,Agent 有 shell,用的人是你自己或小团队 | Skills | 一份 Markdown 换掉两轮摸索,还能把规则写死 |
| 工具要给多个宿主用(IDE、桌面客户端、云端 Agent) | MCP | schema 一处定义,宿主各自发现;这是 MCP 的本职 |
| 宿主没有 shell(浏览器客户端、受限沙箱) | MCP | 没有 shell 就没有 CLI 和 Skills 的执行面 |
| 工具几十上百个 | Skills 或 MCP 加延迟加载 | 静态 schema 会吃掉上下文;分层加载几乎绕不开 |
| 需要严格的参数校验、权限边界、审计 | MCP | 协议层有 schema 和授权扩展;CLI 的参数是字符串 |
| 一次性脚本、探索性任务 | CLI | 什么都不用写,让模型自己 --help |
| 你是网站,想让用户浏览器里的 Agent 操作你的页面 | WebMCP | 其他三者都碰不到页面内的状态 |
一条经验:MCP 和 Skills 不排斥。MCP 定义"能做什么",Skills 定义"这类任务该怎么做"。实测里 MCP 模式的模型有一次多跑了三个复核调用,就是因为没有人告诉它"列表返回的字段已经够用了",而这正是一份 SKILL.md 会写的话。
这个实验没有回答的
- 只有一个任务、一个模型、每种接法 5 次。任务简单到三种接法都能做对,所以它比的是过程成本,不是正确率。要比正确率得换更难的任务。
- 只有 3 个工具。几十个工具时的上下文膨胀和选错工具的概率,这里量不出来。
- MCP 走的是本地 stdio。远程 Streamable HTTP 加授权会带来这里没有的延迟和失败方式。
- 没有测多轮对话里的 prompt cache。三种接法的静态部分都可以缓存,缓存之后静态占用的成本差异会缩小,但运行时摸索的成本不会。
复现
pip install "anthropic>=1.6" "mcp>=2,<3"
export ANTHROPIC_API_KEY=sk-ant-...
curl -O https://waylandz.com/examples/tool_surface_comparison.py
python tool_surface_comparison.py run --mode mcp --trials 5
python tool_surface_comparison.py run --mode cli --trials 5
python tool_surface_comparison.py run --mode skill --trials 5
每种模式的逐次结果会写到 results_<mode>.json。改 TASK 和 SEED 就能换任务;改 SKILL_MD 可以试试"允许批量关闭"之后 Skills 的调用次数会不会降下来。
相关章节:第 4 章 MCP 协议详解(按 2026-07-28 规范写)、第 5 章 Skills 技能系统。如果你还没有自己的 Agent 循环,先看用 Python 写一个能搜索、调用工具、保存状态的 Agent,这个测试台的循环就是从那篇抄来的。