MCP vs CLI for Claude Code Agents: What Actually Breaks

MCP vs CLI for Claude Code Agents: What Actually Breaks

Error: spawn npx ENOENT. That was the whole morning, gone, spent staring at an MCP config that ran fine on my machine and fell over the second a teammate tried it on a fresh install. The launch post framing treats MCP versus CLI as a clean hybrid answer, but the real failures show up somewhere else entirely: permission errors disguised as network problems, subprocesses that lose venv state between calls, servers that quietly behave differently across Claude Desktop, Cursor, and a browser based client. So where does the hybrid framing actually hold, and where does it fall apart once you wire either approach into a live agent stack? Let's get into it.


Openclaw's blog frames the MCP versus CLI debate as a hybrid answer, and honestly, they're mostly right. But the reasoning underneath deserves more scrutiny than a launch post is built to give it. MCP earns its keep when a tool needs structure, auth, and discoverability across multiple clients. CLI earns its keep when the model already knows the command from a billion lines of training data, and you don't want a server process babysitting a task that ls or grep already solves in one call. Neither side of that tradeoff is philosophical. It's operational. It shows up the moment something breaks in CI instead of on your laptop.


What follows isn't really a rebuttal of the Openclaw framing. It's more of a field report: where it holds up, where it gets fuzzy, and what I'd actually tell someone setting up their first agent tooling stack this month. Start with the case MCP makes well.


Where MCP Earns Its Complexity

Where a Sales Query Silently Fails

Where a Sales Query Silently Fails
Step 1
Stakeholder says "check last month's sales"
Step 2
MCP server converts to typed, validated call
Step 3
Query hits DB_CONN readonly user
Step 4: Breaks Here
Missing SELECT permission on new view
Step 5
Generic permission error misread as a network problem
Typed interfaces catch malformed calls, not malformed permissions.

Source: Based on scenario described in article


MCP's pitch has always been the typed interface. A server declares its tools, its input schemas, its expected outputs, and the client discovers all of that at connection time instead of the model guessing at command line syntax it half remembers. That part of the Openclaw post checks out, and it matches what I've seen running MCP servers against Claude Code and Cursor over the past several months. When a non technical stakeholder says "check last month's sales" instead of writing a SQL query, an MCP server with a well scoped query tool turns that sentence into a validated, typed call instead of a string the model concatenates and hopes for the best.


The cross client reuse claim holds up too, with a caveat nobody puts in the marketing copy. Reuse across Claude Desktop, Cursor, and Windsurf assumes those clients implement the same subset of the spec. They don't always agree on transport details, or on how they surface resource lists versus tools. I've had a filesystem MCP server work cleanly with Claude Desktop's stdio transport and then need a separate SSE wrapper just to behave in a browser based client. That's not a protocol failure, it's an ecosystem maturity issue, and it's worth knowing before you promise your team "one server, everywhere."



{
  "mcpServers": {
    "sales-db": {
      "command": "node",
      "args": ["./servers/sales-mcp/index.js"],
      "env": {
        "DB_CONN": "postgres://readonly@localhost:5432/sales"
      }
    }
  }
}

That config file looks trivial until the readonly user in DB_CONN doesn't actually have SELECT on the view you added last sprint, and the MCP server returns a generic permission error that the model then tries to explain to a non technical user as a network problem. Typed interfaces prevent malformed calls. They don't prevent malformed permissions, and that distinction cost me a support ticket I didn't need to file.


MCP is the right call when the tool needs auth, needs to be discovered rather than described, and needs to travel across more than one client. Treat the multi client reuse claim as a starting point, not a guarantee, until you've actually run the same server against two different hosts. That auth and discovery advantage is real. It just says nothing about the tasks MCP handles badly, which is exactly where CLI takes over.


Why CLI Skills Quietly Win on Execution

MCP vs CLI: Where Each Approach Actually Holds Up

MCP vs CLI: Where Each Approach Actually Holds Up
Dimension MCP CLI
Interface type Typed, schema declared Model recalls syntax from training
Best for Auth, discoverability, multi client tools Execution heavy tasks, file ops, package mgmt
Cross client reuse Claimed, but transport details vary by client Not applicable, runs locally per call
Common failure Permission errors read as network issues Subprocess loses venv state between calls
Failure surfaces In CI or on teammate's fresh install In CI or on teammate's fresh install

Source: Based on article analysis of MCP and CLI tradeoffs


The Openclaw post gets this right: execution heavy tasks, file operations, package management, system commands, are usually better served by CLI, because the model already understands the command patterns from training data. This is the part practitioners underrate. Claude Code doesn't need an MCP wrapper around git status or npm install. It needs permission to run them and a sandbox that doesn't let a bad rm -rf ruin your afternoon. Wrapping every shell command in a typed MCP tool adds a layer of indirection that buys you almost nothing when the model's base capability already covers the case.


I've watched this go wrong in the other direction too. A colleague told me about an MCP server they built that exposed a tool called run_python_script, which took a string of code as input and executed it server side. Functionally that's just a CLI call wearing an MCP costume. It inherited every CLI risk, arbitrary code execution, no sandboxing by default, while stacking MCP's connection overhead on top of it. If you're going to shell out, shell out directly and let the agent's existing bash tool handle it with proper permission prompts. Don't launder it through a protocol that implies more safety than it actually delivers.



what actually gets debugged in practice

MCP Behavior Consistency Across Clients

MCP Behavior Consistency Across Clients
Client Transport Support Tool Discovery Resource Listing
Claude Desktop High (stdio) High Medium
Cursor Medium High Medium
Browser Based Client Low (needs SSE wrapper) Medium Low
Green: consistent behavior. Amber: partial support. Red: requires extra workarounds.

Source: Based on article's account of running MCP servers across Claude Desktop, Cursor, and browser based clients

python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt

ModuleNotFoundError: for a missing dependency such as 'psycopg2', in a scenario like this


turns out the agent ran this inside the wrong venv


because a previous CLI skill call didn't preserve shell state


That venv problem is the real argument against pure CLI skills for anything stateful. When Claude Code sessions lose context mid task, this is usually where it happens. A CLI skill invocation spawns a new subprocess, the subprocess doesn't inherit the activated environment from three tool calls ago, and the model has no idea the pip install it just ran landed in system Python instead of the project venv. MCP servers with persistent connections don't have this failure mode, because the server process holds state across calls. That's a genuine structural advantage for MCP, one the Openclaw framing only mentions indirectly under "services that need authentication," when the real driver is often statefulness, not auth at all.


CLI skills win on tasks the model already knows cold. But the moment a task needs persistent state across multiple calls, plain CLI starts fighting you. That's the actual dividing line, more than the file operations versus API calls split the post leans on. It also changes what "discovery" even means once you put the two approaches side by side, which is the last thing worth testing.


Is Tool Discovery Really MCP's Best Argument?


Openclaw's third point, that MCP servers tell the AI what they can do while CLI requires manually describing every command, is technically accurate and practically overstated. Yes, MCP's list_tools call gives a client structured metadata automatically. But Claude Code already handles a huge chunk of CLI discovery for free, because the model was trained on man pages, help output, and thousands of Stack Overflow threads about what tar -xzf does. You don't manually describe git commit to Claude the way you'd describe a bespoke internal API.


Where discovery actually matters is internal, bespoke tooling with no public documentation trail: your company's deploy script, an internal billing service, a proprietary data pipeline. That's exactly where MCP's self describing schema earns its cost, because there's no training data prior for the model to lean on. For anything with real documentation already floating around the internet, the discovery advantage narrows fast. I've seen teams build MCP wrappers around standard Unix tools that added latency and config surface for a discovery benefit that was already close to zero.



a discovery problem MCP actually solves well


internal tool with zero public docs, zero training data prior

class DeployServer: def list_tools(self): return [{ "name": "deploy_to_staging", "description": "Deploys current branch to staging cluster", "inputSchema": { "type": "object", "properties": { "branch": {"type": "string"}, "skip_migrations": {"type": "boolean", "default": False} }, "required": ["branch"] } }]

That's the discovery case that justifies MCP's overhead: a tool the model has never seen described anywhere, with a schema that stops it from guessing wrong on skip_migrations and taking down a database mid deploy. Compare that to wrapping curl in an MCP server so the model can call an API it could already hit directly with the right headers, something I've seen recommended in more than one starter template, and which mostly just adds a process you now have to keep alive.


Tool discovery is MCP's strongest real advantage, but only for tools with no training data footprint. Apply it to anything the model already understands from its base knowledge and you're paying protocol tax for a benefit that was never there. And that's the actual fix for the morning this post started with: the config wasn't broken because MCP or CLI was the wrong protocol. It failed because a stateful, permissioned task got treated as a stateless discovery problem. Match the tool to the failure mode, auth and cross client reuse to MCP, execution the model already knows to CLI, state to whichever one actually holds it, and the hybrid framing stops being a slogan and starts being a checklist you can actually debug against.