Blog

MCP, WebMCP, CLI, Skills: Which Tool Surface for the Same Task?

September 17, 2026

中文

Three things I would tell someone choosing between these today, based on the measurements below.

  1. When the tool already has a command line and the agent can run a shell, a SKILL.md is less work than wrapping an MCP server, and it is enough. "Less work" means implementation and maintenance: one Markdown file against a server you keep running. On runtime cost MCP measured lowest; what Skills removed was the two rounds of "read --help" that the bare CLI paid on every run, and it finished with the smallest context of the three.
  2. When you need reuse across hosts, schema-validated arguments, or a host that has no shell at all, use MCP. It had the fewest model calls and the shortest wall time, at the price of writing and maintaining a server. The Python SDK I used also drops the reason text from tool exceptions by default (details below).
  3. WebMCP is not a substitute for any of the three. It exists only in the browser, only while a page is open, and only for agents that reach page tools through the interface the browser provides. It answers "how does a website hand its features to the agent in the user's browser", not "how does my agent reach backend tools".

All four words are on conference agendas this month; this week's AGNTCon + MCPCon Europe has a session titled "Three Doors to One Tool: MCP vs WebMCP vs CLI". This post puts one task through the first three and counts, then explains why the fourth cannot share the table.

The experiment: one task, three surfaces

The task. A SQLite table with 8 issues. The agent must close every issue that is assigned to kim, is open, and has not been updated for more than 30 days, with the exact comment Closed for inactivity, and must not touch anything else. The correct answer is to close 101, 102 and 107.

Four traps are planted: one of kim's issues was updated 23 days ago (not stale, must stay open); one of kim's is already closed; one is assigned to kimberly (a prefix match would hit it); one is old but assigned to someone else. After each run a script compares the whole table with the expected end state, every field of every row. An extra row, a missing row, or any changed field counts as failure.

Three surfaces, all driving the same database through the same functions:

SurfaceWhat the model seesHow it executes
MCPJSON Schemas for three tools (list_issues_tool, get_issue_tool, close_issue_tool), generated by MCPServer from the Python mcp SDK v2, over stdioEach tool call is forwarded by the harness to the MCP server
CLIOne bash tool plus the sentence "an issues command line tool is installed"The model runs issues --help and works it out
SkillsThe same bash tool plus a 250-word SKILL.md in the system promptNo exploration; it follows the commands and rules in SKILL.md

The SKILL.md follows the Agent Skills specification. Its body is three command descriptions and three rules: always use --json, filter server-side with --assignee and --state, close one issue per command and read the response.

Measurements. Per run: model calls, tool calls, tool errors, cumulative input and output tokens, input tokens of the final call (the context footprint at the end of the task), static footprint (system prompt plus tool definitions before the first call, from count_tokens), wall time, and whether the database end state is exactly right.

The model is Claude Opus 5 with default thinking settings, five runs per surface, 15 runs total, about a dollar all in. The whole harness is one Python file containing the seed data, the CLI, the MCP server, the SKILL.md and the runner: tool_surface_comparison.py.

Results

All 15 runs were correct. Every difference is in the process:

SurfaceCorrectModel callsTool callsInput tokensOutput tokensFinal contextStaticWall time
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

(Means over 5 runs. Token columns are the per-run cumulative input and output, averaged. This is the round rerun after switching to the whole-table verifier; an earlier round with a verifier that checked only some fields averaged MCP 3.2 / CLI 6.4 / Skills 6.0, also 15/15 correct. The CLI difference between the rounds comes from one 10-call outlier in this round, described under failure modes.)

A typical trace for each surface, taken from the second run of each:

MCP (3 model calls):

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"})

Call one lists the issues, call two fires all three closes at once (parallel tool use), call three writes the summary.

CLI (7 model calls):

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

The first two calls are pure exploration. The third and fourth list the issues (all of them, then filtered), the fifth closes all three in one shell line, the sixth is a verification pass.

Skills (6 model calls):

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

No exploration; the very first command carries the filters. But the three closes took three separate calls, because rule three in the SKILL.md says "close one issue per command and read the response".

Reading the numbers

Model calls: discovery is the biggest line item

Of the 4.2 extra model calls the bare CLI spends over MCP, 2 go to --help in all five runs without exception; the rest went to defensive work such as listing twice and scripting the date arithmetic (see failure modes below). That is the direct price of "the model does not know how to use the tool", and every new session pays it again.

MCP pays it up front: the tool schemas are in the context from the first call and the model goes straight to choosing arguments. Skills also pay up front, by putting the usage into the system prompt as prose. The difference is that MCP's schema is machine-generated and machine-validated, while a SKILL.md is written by a person, and how well it is written decides how the model behaves.

Two of Skills' six calls were forced by my own rule. "Close one per command" made the model split what could be one shell line into three round trips. Change the rule to "you may close several issues in one command" and the round trips should drop; by how much I have not measured. On this task the "one at a time" rule cost two extra round trips. When you write a SKILL.md, ask of every constraint whether it adds a round trip.

Context footprint: separate static from runtime

The static footprint is smallest for CLI (415 tokens), just one bash tool definition. But its final context is the largest (3,623), because both rounds of --help output stay in the conversation and are billed again on every later call.

MCP is 745 static, only about 330 tokens more than the single bash tool definition, and 2,195 at the end. Skills is 714 static and 1,976 at the end, the smallest of the three: no --help output, and the tool returns compact JSON.

This experiment has three tools. With dozens or hundreds, MCP's static footprint grows linearly, while Skills are layered by design: the Agent Skills spec keeps name plus description resident (about 100 tokens per skill) and loads the body only on activation. On the MCP side, the official roadmap of 2026-08-22 lists "progressive tool discovery" as a priority, but as of the 2026-07-28 specification it is a plan, not a shipped capability. To layer MCP tools today you do it on the host side with deferred loading; the method is in Chapter 38, "Deferred Tool Loading and Tool Search".

Wall time: in this experiment it went to model round trips

The wall times sort in the same order as the model calls. Tool execution itself took milliseconds here, so nearly all the time is model round trips. To be faster, cut round trips: let the model know the right call on the first try, or let it make several calls at once.

Failure modes: nothing failed, but the variance differs

All 15 passed, so this task cannot separate "which is more reliable". The variance still carries information:

  • CLI run 4 took 10 calls and 35,523 input tokens (the other four were between 8,800 and 11,500). The model first wrote a Python snippet to compute the date differences, then ran the close commands after a cd /tmp. The harness CLI locates its database by a relative path, so this created an empty database in /tmp and the closes "succeeded" without changing anything. The model noticed, grepped the harness source code for the database path, went back to the right directory, redid the three closes, and deleted the stray file in /tmp. The end state was correct, but along the way it read code unrelated to the task and created and cleaned up a file outside the working directory. A bare shell gives the model freedom, and it spends it on defensive work and on cleaning up after itself, at your token expense. The relative-path problem, an old CLI hazard, cannot occur in MCP mode (the tool runs in a fixed process); Skills mode has the same shell, it did not happen in these five runs, and nothing prevents it. Run 5 also scripted the date arithmetic, without the wrong directory.
  • MCP run 5 re-checked each of the three issues with get_issue_tool before closing, costing 1 extra call and about 2,400 tokens. The tool was in the schema, so the model used it.
  • The five Skills traces are almost identical, the lowest variance of the three. Once the rules are in the prompt, the behavior is pinned.

There is one more failure mode I hit while building the harness, and it belongs to the SDK, not the model. With MCPServer from the Python mcp SDK v2, a tool that does raise ValueError("issue 104 is already closed") reaches the model as:

Error executing tool close_issue_tool

The reason is gone. To let the model see it you have to raise the SDK's own ToolError. The bare CLI has no such problem: stderr and the exit code go into the context as they are. This is the default of the Python mcp SDK v2 MCPServer I tested, not a rule of the protocol, and another SDK or version may behave differently. The trade is that the SDK standardizes the shape of errors, and in exchange you must report them its way, or the model retries without knowing why it failed.

Why WebMCP is not in the table

WebMCP is a browser standard draft driven by Google and Microsoft in a W3C community group, not yet on the standards track. Chrome runs an origin trial from version 149, and local development can enable it with chrome://flags/#enable-webmcp-testing. What it does is let a web page register tools through document.modelContext.registerTool() (the same fields as MCP: name, description, inputSchema, execute, plus annotations such as readOnlyHint), or mark up a <form> with attributes like toolname and tooldescription so the browser turns the form into a tool automatically.

The three fundamental differences from MCP are stated plainly in Chrome's documentation:

MCPWebMCP
Where the tool runsYour backend processThe page in the user's browser
Who can call itAny MCP clientAgents that reach page tools through the browser's interface (built in, extension, or embedded in the page)
LifetimeAs long as the processGone when the page closes
Identity and sessionYour own OAuthThe page's existing logged-in session and cookies

So it cannot join this experiment: my harness is an agent in a Python process, with no browser and no page. Measuring WebMCP means running a page agent inside Chrome, which is a different experiment answering a different question: should a site owner add WebMCP to their pages, not how an agent developer should connect tools.

If you are both, Chrome's guidance is to use both: MCP for core business logic and background work, WebMCP for interactions while the user is on the page.

Choosing

SituationPickWhy
The tool has a CLI, the agent has a shell, the users are you or a small teamSkillsOne Markdown file replaces two rounds of exploration and lets you pin the rules
The tool must serve several hosts (IDE, desktop client, cloud agent)MCPDefine the schema once, let each host discover it; this is MCP's job
The host has no shell (browser client, restricted sandbox)MCPWithout a shell there is no execution surface for CLI or Skills
Dozens or hundreds of toolsSkills, or MCP with deferred loadingStatic schemas eat the context; some form of layering becomes necessary
Strict argument validation, permission boundaries, auditMCPSchemas and the authorization extension live at the protocol level; CLI arguments are strings
One-off scripts, exploratory tasksCLIWrite nothing; let the model --help its way through
You are a website and want the agent in your users' browser to operate your pagesWebMCPNone of the other three can reach in-page state

One lesson from the traces: MCP and Skills are not exclusive. MCP defines what can be done, Skills define how this kind of task should be done. The MCP run that spent three extra verification calls did so because nothing told the model "the list output already has every field you need", which is exactly the sentence a SKILL.md would contain.

What this experiment does not answer

  • One task, one model, five runs per surface. The task is easy enough that all three surfaces get it right, so this compares process cost, not accuracy. A harder task is needed to compare accuracy.
  • Three tools only. Context growth and wrong-tool rates at dozens of tools are not measured here.
  • MCP ran over local stdio. Remote Streamable HTTP with authorization adds latency and failure modes absent here.
  • No prompt caching across turns. The static part of all three surfaces is cacheable, which narrows the static cost gap; the runtime exploration cost does not shrink.

Reproduce it

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

Per-run results land in results_<mode>.json. Change TASK and SEED to swap the task; change SKILL_MD to see whether allowing batched closes brings the Skills call count down.

Related chapters: Chapter 4, MCP Protocol Deep Dive (written against the 2026-07-28 specification) and Chapter 5, Skills System. If you do not have an agent loop of your own yet, start with Build an Agent That Searches, Calls Tools, and Keeps State; the loop in this harness is lifted from that post.