博客

MCP、WebMCP、CLI、Skills:同一个任务该选哪种工具?

2026年9月17日

English

如果今天有人问我该选哪个,我会给下面三条建议,依据是后面的实测。

  1. 工具已经有命令行、Agent 又能跑 shell 的时候,写一份 SKILL.md 比包一个 MCP server 省事,而且够用。 "省事"指实现和维护成本:一份 Markdown 对一个要长期维护的 server。运行成本上实测 MCP 最低;Skills 省掉的是裸 CLI 每次都要花的两轮"读 --help",最终上下文也是三者里最小的。
  2. 需要跨宿主复用、需要参数 schema 校验、宿主根本没有 shell 的时候,用 MCP。 它是三者里模型调用最少、耗时最短的,代价是你得写并维护一个 server;另外我用的 Python SDK 默认会吞掉工具异常的原因文本(后面有细节)。
  3. 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_toolget_issue_toolclose_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结束时上下文静态占用耗时
MCP5/53.24.64,7158682,19574515.9 s
CLI5/57.46.815,6801,4503,62341525.7 s
Skills5/56.05.08,7529051,97671418.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 规范里 namedescription 常驻(约 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 一样:namedescriptioninputSchemaexecute,外加 readOnlyHint 之类的注解),或者给一个 <form> 加上 toolnametooldescription 这类属性,让浏览器自动把表单变成工具。

和 MCP 的三个根本差别,Chrome 的文档写得很直接:

MCPWebMCP
工具在哪运行你的后端进程用户浏览器里的页面
谁能调用任何 MCP client通过浏览器接口访问页面工具的 Agent(内置、扩展或页内嵌入)
活多久进程活多久它活多久页面关了就没了
身份和会话你自己做 OAuth直接用页面已登录的 session 和 cookie

所以它没法进这个实验:我的测试台是一个 Python 进程里的 Agent,没有浏览器,也没有页面。要测 WebMCP 得在 Chrome 里跑一个页面 Agent,那是另一个实验,测的也是另一个问题:"网站主"该不该给自己的页面装 WebMCP,而不是"Agent 开发者"该怎么接工具。

如果你两边都是,Chrome 文档的建议是并用:核心业务逻辑和后台任务走 MCP,用户正在页面上时的交互走 WebMCP。

选型表

场景原因
工具已有 CLI,Agent 有 shell,用的人是你自己或小团队Skills一份 Markdown 换掉两轮摸索,还能把规则写死
工具要给多个宿主用(IDE、桌面客户端、云端 Agent)MCPschema 一处定义,宿主各自发现;这是 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。改 TASKSEED 就能换任务;改 SKILL_MD 可以试试"允许批量关闭"之后 Skills 的调用次数会不会降下来。

相关章节:第 4 章 MCP 协议详解(按 2026-07-28 规范写)、第 5 章 Skills 技能系统。如果你还没有自己的 Agent 循环,先看用 Python 写一个能搜索、调用工具、保存状态的 Agent,这个测试台的循环就是从那篇抄来的。