The ThoughtWorks Technology Radar Volume 32 put "MCP by default" in their Caution ring. Their argument is precise: MCP adds real value for structured tool contracts, OAuth-based authentication boundaries, and governed multi-tenant access. It also introduces what Justin Poehnelt calls an "abstraction tax" — every protocol layer between an agent and an API loses fidelity, and for complex APIs those losses compound.
Simon Willison sharpens it further: "almost everything I might achieve with an MCP can be handled by a CLI tool instead."
We build an open-source cloud security reasoning engine called Stave. We have an MCP server. We also have a CLI. And the CLI is the integration surface that matters. Here's why — and the architectural pattern that made the decision easy.
The abstraction tax in practice
MCP is a protocol. It handles discovery (what tools exist), invocation (call this tool with these parameters), and transport (stdio, SSE, HTTP). For an AI agent that needs to find and use tools dynamically across multiple servers maintained by different teams, this is genuine value. The discovery problem alone justifies the protocol in multi-vendor environments.
But most tools aren't in multi-vendor environments. Most tools are called by one agent, in one pipeline, doing one job. And for that case, MCP is a translation layer between two systems that could have talked directly.
The fidelity loss is real. MCP tool schemas are JSON Schema, which is expressive enough for parameter types but lossy for things like streaming output, progress indicators, and structured error hierarchies. A CLI can emit a JSON object with a status field, an errors array with structured error codes, and a findings array with typed domain objects — all in one predictable output. An MCP tool returns a content array of text blocks. The agent has to parse the text to extract the structure. Every parse is a fidelity boundary.
What a well-designed CLI gives agents for free
ThoughtWorks mentions "good --help output, structured JSON responses and predictable error handling." That's worth unpacking, because each of those properties maps to something MCP tries to provide via protocol, but a CLI provides via convention:
Discovery. MCP has tools/list. A CLI has --help and man pages. For an AI agent, stave --help produces a structured description of every subcommand, flag, and output format. The agent reads it once. No handshake, no transport negotiation, no capability exchange.
Invocation. MCP has tools/call with JSON parameters. A CLI has flags and arguments. stave evaluate --snapshot ./snapshot.json --format json is one shell command. The agent constructs it from the help output. No serialization layer, no transport encoding.
Output. MCP returns content blocks. A CLI writes to stdout. stave evaluate produces a JSON file — out.v0.1.json — with a documented, versioned, JSON Schema-validated structure. The agent reads the file. No parsing MCP content blocks, no extracting text from protocol wrappers.
Error handling. MCP has error codes in the JSON-RPC envelope. A CLI has exit codes and stderr. Exit 0 means success; exit 1 means evaluation failed; exit 2 means invalid input. Stderr carries the human-readable message. Stdout carries the machine-readable output. Clear separation, no protocol overhead.
The contract-first pattern
Here's the architectural insight that made our decision easy.
Stave's integration surface isn't the CLI and it isn't the MCP server. It's the contract — out.v0.1.json. This is a JSON file with a documented schema (JSON Schema Draft 2020-12), versioned by the SchemaOutput constant, validated at runtime, and stable across releases. Every finding, every control verdict, every compound risk chain lives in this file.
The CLI produces the contract. The MCP server also produces the contract. A Steampipe plugin reads the contract. A Powerpipe mod reads the contract. A Flowpipe pipeline reads the contract. The contract is the integration surface. Everything else — CLI, MCP, SQL, HCL — is a transport that wraps it.
This is the pattern ThoughtWorks is implicitly recommending:
┌── CLI (direct)
│
Contract (JSON) ────┼── MCP server (protocol)
│
├── Steampipe plugin (SQL)
│
└── Powerpipe mod (HCL/DuckDB)
The contract came first. The transports came second. If you design the contract well — stable schema, versioned, validated, documented — then adding MCP is a thin wrapper, not a re-architecture. And removing MCP changes nothing about the system's integration capability. The contract survives.
If you design around MCP first, the protocol is the contract, and you've coupled every consumer to a transport layer they may not need.
When MCP earns its keep
None of this means MCP is wrong. ThoughtWorks is right that it adds real value in specific cases:
Multi-tenant access governance. When an MCP server mediates access to a shared resource with per-user OAuth scopes, the protocol boundary is doing real work — it's an authorization layer, not just a transport. A CLI can't do this without reimplementing the same boundary.
Dynamic tool discovery across organizations. When an agent needs to discover tools it's never seen before, published by teams it doesn't coordinate with, MCP's tools/list + schema mechanism is the right primitive. A CLI's --help output works for known tools; MCP works for unknown ones.
Hosted, stateful services. When the tool maintains server-side state (sessions, caches, connections) that shouldn't be managed by the caller, MCP's server lifecycle makes sense. A CLI is stateless by design; if your tool isn't, MCP manages the statefulness.
For Stave, we have an MCP server because AI agent platforms (Claude Desktop, Cursor, Windsurf) speak MCP natively. If an agent wants to ask "what's the security posture of this AWS snapshot?" without constructing a shell command, the MCP server handles it. That's a real use case — but it's a convenience layer over the contract, not the integration architecture.
Asking the Right Questions
Before reaching for MCP, ask three questions:
Does the consumer know about the tool in advance? If yes — if you're building a pipeline that calls a specific tool — a CLI invocation is simpler, faster, and loses no fidelity. MCP's discovery mechanism is solving a problem you don't have.
Does the interaction require server-side state or authorization? If yes — if there's an OAuth boundary, a persistent session, or per-user scoping — MCP's protocol lifecycle earns its keep. A CLI would have to reimplement that boundary.
Is the contract documented and versioned independently of the transport? If no — if the only way to know what the tool produces is to read the MCP tool schema — then the tool is coupled to the protocol. Document the contract first. Make it a file, a schema, an artifact. Then decide whether to wrap it in MCP, a CLI, both, or neither.
The design that survives: contract-first, protocol-second.
Stave is an open-source cloud security reasoning engine that evaluates configuration snapshots against safety invariants — CEL predicates over JSON Schema-validated facts. The output contract (out.v0.1.json) is consumed by CLI, MCP, Steampipe, Powerpipe, and Flowpipe. Star it on GitHub.
ThoughtWorks Technology Radar Vol 32, "MCP by default" — full entry
Top comments (0)