What 'MCP Is Dead' Gets Wrong
Abstract
Every useful tool operates on a resource: a database, a directory, a file. CLI tools declare this connection implicitly through documentation. Skills declare it by embedding paths in prose. MCP declares it through structured configuration. Beyond declarations, there is a second problem: encapsulation. CLI workflows scatter behavior, state, and interface across separate artifacts. Each seam is a coordination point where errors accumulate. MCP servers bundle these concerns into a single process, eliminating the seams. The “MCP is dead” argument ignores both problems.
The Discourse
Two camps are currently arguing that MCP (Model Context Protocol) is obsolete:
- “CLI replaces MCP.” LLMs can already call CLI tools via shell commands. Why add a protocol layer?
- “Skills replace MCP.” Prompt-based workflows can embed tool knowledge directly. Why separate the tooling?
Each argument gets something right and something wrong. They correctly observe that LLMs can use CLI tools and Skills effectively. They incorrectly conclude that this eliminates the need for MCP. The error is treating the interface as the system.
These are not straw-man objections; representative versions of them have been made quite explicitly:
- Eric Holmes argues in “MCP is dead. Long live the CLI” that MCP offers “no real-world benefit” over existing CLI + docs workflows, and that CLIs win on interoperability, composability, authentication, and debuggability.
- Mario Zechner argues in “Local coding agents: the CLI is all you need” that local coding agents should usually prefer a “good CLI tool with a good README,” because MCP tool descriptions are injected into context up front while CLI documentation can be read on demand; his
piagent deliberately has no MCP support for the same reason. - Anthropic’s official skills announcement strengthens the “Skills replace MCP” intuition by describing skills as portable bundles of instructions, scripts, and resources that are loaded only when relevant; the Claude Code skills docs make the same pattern operational.
- Maxim’s “Skills vs MCP vs API: Choosing the Right Tool Integration Pattern” makes the more specific critique: many early MCP servers were just glorified
curlcommands, overengineered CLI wrappers, or stateless integrations better served by Skills plus shell access.
Two Kinds of CLI
Before examining the “CLI replaces MCP” argument, we need to separate two different categories of CLI tools:
Cloud CLIs (gh, gcloud, aws, stripe) are thin wrappers around remote APIs. Their defining characteristic is isolation: the consumer and the service run in separate environments, separated by a network boundary. The same applies to Docker-based workflows, where the container provides the same isolation. In both cases, resource binding, state management, and interface contracts live on the server side. Encapsulation comes for free. Saying “cloud CLIs replace MCP” is incoherent: they already have the isolation that MCP provides for local tools.
Local CLIs (sqlite3, jq, ripgrep, ffmpeg) operate on local files. No network, no container, no environment boundary. Everything runs in the same process space. They are fast, composable, and self-contained. This is where the “CLI replaces MCP” argument lives: why wrap sqlite3 in an MCP server when the LLM can just call it directly?
The rest of this post examines what local CLIs lack, and why it matters.
The Encapsulation Gap
“Local CLI” is not a monolithic category. These tools vary widely in how well they encapsulate their concerns:
| Dimension | Ad-hoc script | Mature CLI (ffmpeg, sqlite3) | MCP server |
|---|---|---|---|
| Interface declaration | None / README prose | --help text | JSON Schema (typed) |
| Output contract | Free-form stdout | Semi-structured (table/TSV) | Structured JSON |
| Resource binding | Paths hardcoded in script | CLI argument / env var | Server-internal config |
| State management | Consumer (LLM) responsible | Consumer responsible | Server-internal |
| Discoverability | Read source code | --help / man page | Programmatic (tools/list) |
Mature CLIs score well on interface and output (the top two rows). But resource binding and state management (the bottom two) remain the consumer’s responsibility regardless of how polished the CLI is. These two gaps are where the “CLI replaces MCP” argument breaks down.
Resource binding: the hidden path
A local CLI has no server. But it still has a path declaration: the resource it operates on must be specified somewhere.
sqlite3 ~/.che-errata/errata.sqlite "SELECT * FROM errata"
rg "pattern" ~/project/src/
jq '.data[]' /tmp/response.json
Where does the LLM learn these paths? Three possible sources, all natural language:
| Source | Format | Example |
|---|---|---|
CLAUDE.md | Prose | ”The errata database is at ~/.che-errata/errata.sqlite” |
| Skill | Prose | ”Run errata query "text" --json to find matches…” |
| Conversation | Ad hoc | User says “check the database in ~/.che-errata/” |
Skills go further: they embed not just paths but entire dependency graphs in prose, including binary locations, database paths, target file globs, and workflow steps. This is a dependency declaration written in natural language that the LLM must parse to extract the same information an MCP .mcp.json provides as structured JSON.
flowchart TD
subgraph cli ["CLI / Skill Approach"]
S1["CLAUDE.md / SKILL.md<br/><em>prose path declaration</em>"]
P1["LLM parses prose<br/><em>probabilistic</em>"]
C1["Bash: errata query 'text'"]
S1 --> P1 --> C1
end
subgraph mcp ["MCP Approach"]
S2[".mcp.json<br/><em>structured config</em>"]
P2["Client reads JSON<br/><em>deterministic</em>"]
C2["errata.query#40;text#41;"]
S2 --> P2 --> C2
end
C1 --> DB[(Database)]
C2 --> DB
Both paths reach the same database. The difference is how the connection is declared: prose that the LLM must interpret, or structured configuration that the client reads deterministically. The local CLI is not a “hidden server.” But it has a hidden path: a resource location conveyed through documentation rather than declared in configuration.
State management: the invisible seam
A CLI-based workflow scatters its concerns across multiple artifacts:
flowchart TD
A["① CLAUDE.md<br/><em>find binary location</em>"]
B["② Skill<br/><em>follow multi-step recipe</em>"]
C["③ CLI binary<br/><em>construct correct Bash</em>"]
D["④ Config path<br/><em>locate state file</em>"]
E["⑤ stdout<br/><em>parse output correctly</em>"]
A --> LLM
B --> LLM
C --> LLM
D --> LLM
E --> LLM
LLM["LLM must coordinate all 5<br/><em>each step is probabilistic</em>"]
Each arrow is a coordination point where the LLM must correctly parse unstructured information and act on it. Each point has a small failure probability. But probabilities multiply:
| Steps in chain | Per-step reliability | Overall reliability |
|---|---|---|
| 2 (MCP: call tool, read response) | 95% | 90.2% |
| 5 (CLI: find binary, follow Skill, construct command, locate state, parse output) | 95% | 77.4% |
This is not hypothetical. In a real proofreading workflow, I observed the following failure mode: the Skill instructed the LLM to persist each correction to a database after applying it. When the session’s context window compacted mid-workflow, the LLM lost the Skill instructions and silently stopped persisting corrections. Twelve fixes were applied to files but never recorded in the database. The next session had no knowledge of them.
The error was invisible. No step failed. The LLM simply forgot one link in a chain held together by prose instructions.
An MCP server eliminates these seams by bundling behavior, state, and interface into a single process:
flowchart LR
A[".mcp.json<br/><em>deterministic config</em>"] --> B["MCP Server<br/><em>behavior + state + interface<br/>bundled in one process</em>"]
B --> C["Typed JSON response"]
The LLM doesn’t need to know where the database is. The server knows. The LLM doesn’t need instructions to “also persist this.” The server handles persistence internally. The LLM doesn’t need to parse stdout. The server returns typed JSON. Every seam you eliminate is an error that can no longer accumulate.
Memory: a special case of state management
The “memory” problem (tools that accumulate knowledge across sessions) makes the state management gap concrete.
With CLI + Skill, state management is orchestrated by the LLM: the Skill says “after step 6, also run errata add.” If the LLM skips the step, state is lost. If the session ends unexpectedly, incomplete state is lost. The recipe says “remember to save your work,” but the recipe cannot enforce it.
With MCP, state management is internal to the server: the server writes to its own database as part of handling the tool call. The LLM doesn’t need to remember an extra step. State persists regardless of what happens to the LLM’s session.
This is why encapsulation matters for LLM tooling specifically. LLMs have finite context windows, sessions that compact or terminate, and attention that degrades over long workflows. Every concern that is externalized (every “also remember to do X” in a Skill) is a concern that will eventually be dropped. Encapsulated concerns survive because they don’t depend on the LLM remembering them.
The Recurring Pattern
The two arguments above (explicit resource binding and encapsulated state) are not novel. They are the recurring pattern of software engineering: making implicit knowledge explicit.
| Era | Implicit | Explicit |
|---|---|---|
| Compilation | Manual memory layout | Compiler manages allocation |
| Dependencies | ”Download version X from site Y” | package.json / requirements.txt |
| APIs | ”Send a POST to /users with fields…” | OpenAPI / GraphQL schema |
| Configuration | Hardcoded paths in source | Environment variables, .env |
| LLM tooling | ”Run sqlite3 path/db and parse stdout” | MCP tool with typed parameters |
“CLI replaces MCP” is equivalent to “shell scripts replace APIs.” “Skills replace MCP” is equivalent to “README files replace package.json.” Both accomplish the same task. One requires the consumer to parse unstructured text. The other provides a typed contract.
This also clarifies the relationship between Skills and MCP. They are not competing alternatives. They operate at different layers:
flowchart TD
A["<b>Skills</b><br/>Workflow orchestration<br/><em>'When to do what, in order'</em>"]
B["<b>MCP / CLI Tools</b><br/>Capability interface<br/><em>'What operations exist'</em>"]
C["<b>Resources</b><br/>Databases, APIs, files<br/><em>'Where the data lives'</em>"]
A -->|"references"| B
B -->|"connects to"| C
A Skill references MCP tools. It doesn’t replace them, any more than a recipe replaces a kitchen. Removing the MCP layer doesn’t eliminate the need for it. It pushes the interface contract into documentation, where it becomes implicit, fragile, and model-dependent. When path declarations, interface contracts, tool discovery, and lifecycle management are bundled into a single declaration, you get something that software engineers have a name for: a software package. MCP servers are packages. CLI tools with their scattered config files and documentation are not.
Different audiences, different feature sets
There is a subtler point that the “CLI vs MCP” framing obscures: CLI and MCP are not the same functionality at different levels of formality. They serve different audiences with different feature sets, sharing a common library underneath.
A CLI tool is for humans. It works without AI. Commands like errata history, errata stats, and errata export exist because a human might want to browse the database, check coverage, or pipe output into another tool. These are browsing and debugging affordances that make no sense as MCP tools — an agent doesn’t need --help text or pretty-printed tables.
An MCP server is for agents. It can’t work without AI. Operations like correct_and_persist (atomically apply a correction and write it to the database in one tool call) exist because an agent needs compound operations that eliminate coordination points. A human wouldn’t call correct_and_persist from a terminal — they’d edit the file and run errata add separately. But for an agent, that separation is exactly the kind of seam where errors accumulate.
flowchart TD
subgraph cli ["CLI — for humans"]
A1["errata query"]
A2["errata history"]
A3["errata stats"]
A4["errata export"]
end
subgraph mcp ["MCP — for agents"]
B1["errata.query"]
B2["errata.correct_and_persist"]
B3["errata.context_for_text"]
end
A1 & A2 & A3 & A4 --> LIB
B1 & B2 & B3 --> LIB
LIB["<b>ErrataLib</b><br/>Shared Swift library<br/>Models · Store · SQLite"]
The shared library (ErrataLib) contains the data models, database access, and business logic. The CLI and MCP server are thin wrappers that present this library to their respective audiences. The CLI adds human affordances (formatted tables, --help, composability with shell pipes). The MCP server adds agent affordances (compound operations, typed JSON, encapsulated state).
This is why “CLI replaces MCP” misses the point twice over. First, it ignores the encapsulation gap described above. Second, it assumes CLI and MCP expose the same operations — when in practice, the operations that matter to a human (history, stats, export) and the operations that matter to an agent (correct_and_persist, context_for_text) barely overlap.
What the Critics Get Right
The argument above defends two principles (explicit declarations and encapsulation), not MCP specifically. This distinction matters, because MCP has real problems that its critics correctly identify.
Authentication is incomplete. MCP has no standardized auth model. Developers must handle API keys, OAuth flows, and multi-user isolation themselves. In multi-tenant scenarios, you may need a separate MCP server instance per user. This is harder than passing a --token flag to a CLI.
Security surface is real. Research has documented prompt injection through MCP tool responses, tool descriptions that misrepresent actual behavior, and widespread cryptographic misuse in public MCP servers. These are not theoretical risks.
The protocol may not win. OpenAI’s function calling, Google’s A2A, and various agent frameworks are competing standards. MCP may end up as middleware, or it may be superseded entirely. The ecosystem has not converged.
Context cost is real. MCP tool descriptions are injected into the LLM’s context at session start, consuming tokens whether or not the tools are used. CLI documentation can be read on demand. This is a real efficiency advantage for large tool registries, though it is a protocol optimization problem, not an argument against structured declarations.
CLI has real strengths. Composability (cat | jq | grep has no MCP equivalent), debuggability (run a command manually and see exactly what happens), and lower overhead (a binary, not a server process) are real advantages for simple, one-shot operations.
These are legitimate criticisms. But notice what they criticize: protocol maturity and trade-offs, not architectural direction. The auth problem is solvable (HTTP didn’t have standardized auth at first either). The security problems affect any tool-calling system. A malicious CLI binary is at least as dangerous as a malicious MCP server, and harder to audit because its interface is unstructured. The ecosystem competition is a standards war, not evidence that the concept is wrong. CLI’s strengths in simplicity do not address the error accumulation problem that emerges in complex, stateful workflows.
The question is whether the eventual winner (MCP, A2A, or something else) will look more like CLI or more like a structured protocol with explicit declarations and encapsulated servers. Every competing standard answers this the same way: typed tool schemas, structured configuration, server-managed state. None of them propose going back to --help output and prose documentation.
MCP may not be the final protocol. But “let the LLM parse --help and follow prose instructions” is not the future either. The direction (from implicit to explicit, from scattered to encapsulated) is permanent. The specific protocol is negotiable.
Conclusion
CLI and Skills work. For simple operations (querying a database, searching a directory, transforming a file), a good CLI with a good README may be all you need. The critics are right about that.
But the “MCP is dead” argument stops there, and the real question starts further down. When workflows grow complex, when tools need to coordinate, when state must persist across sessions, the concerns that CLI and Skills scatter across prose, config files, and LLM memory become failure points. Each seam is invisible until it breaks. Each break is silent.
MCP, or whatever replaces it, bundles these concerns into software: explicit declarations, typed contracts, encapsulated state. The protocol is debatable. The direction is not.
This post emerged from building 5 MCP servers, then building a CLI tool on the same domain, and realizing they don’t replace each other — they serve different audiences with different needs, sharing a common library underneath.