Why I Replaced My MCP Servers With CLIs
Eighteen months in, my agent stack has fewer MCP servers than when I started — and more capability. The honest argument is not the one you've been hearing.
By Nick Meinhold & Claude
TL;DR: Through early 2026 my agent stack accumulated MCP servers — Gmail, Drive, Calendar, then candidates for Signal, WhatsApp, Telegram, Messenger, Outline, Kan. By June I'd reversed the trajectory: zero chat-app MCPs, every replaced with a CLI Claude shells out to. The capability went up. The honest argument for the swap isn't the seductive one ("CLIs cost zero context"). It's three less-glamorous ones: composability, output control, and freedom from the per-project plugin toggle.

The setup
Like everyone else in late 2025, I started attaching MCP servers to my Claude account the moment they shipped. Anthropic put Gmail, Drive, and Calendar in front of me — connect, authorize, done. Each gave the agent a fully-typed tool list it could call. The marketing was clear: this is the model-native way for an LLM to use a service.
Six months later I was looking at a different problem. I wanted Claude to read and send Signal messages. WhatsApp messages. Telegram DMs from my user account. Outline wiki edits. Kan board updates. None of those had a usable hosted MCP — and the unofficial ones I tried (Signal MCP, the Dart kan MCP) were rough, abandoned, or both. So I started building CLIs instead. node ~/.claude/cli-tools/signal/signal.mjs. node ~/.claude/cli-tools/whatsapp/whatsapp.mjs. Each one a small JS/Python file with a --help that documents itself, an .env-sourced credential, and JSON to stdout.
Once I had four chat CLIs working, the comparison with the Gmail MCP got embarrassing. The CLIs were nicer in every direction. So I replaced Gmail too. Then Drive. Then Calendar. Today my Connectors page on claude.ai is empty.
What they look like
Four chat platforms, four one-liners. Each authenticates against an .env sourced once per shell, each returns JSON to stdout, each is two lines of CLAUDE.md to teach Claude.
gmail send --to "you@example.com" --subject "hi" --body "from a CLI"
telegram send --to "me" --text "from a CLI" # me = Saved Messages
signal send --to "+61400000000" --text "from a CLI"
whatsapp send --to "61400000000@s.whatsapp.net" --text "from a CLI"
(There's also a messenger CLI of the same shape — Facebook DMs over the unofficial MQTT chat protocol. Same pattern: cookies in .env-adjacent storage, JSON to stdout, no MCP equivalent that actually works.)
The GIF at the top of this post shows all four firing. Nothing surprising in the shape; that's the point.
The honest design tension
I want to be precise about the trade-off, because "CLIs are cheaper than MCPs" is the seductive-but-shaky version of the argument. Let me say what isn't true first.
The claim "CLIs cost zero context" is wrong. Every CLI Claude knows about is described in CLAUDE.md, which loads into the system prompt every session. My gmail CLI bullet is ~250 chars after a trim today; the original was ~900. The MCP it replaced shipped a schema in the tool list every turn — bigger, but only by a constant factor, and both are subject to prompt caching.
So if the cost is roughly comparable at setup time, where do CLIs actually win?
1. Composability with the rest of Unix
CLI stdout flows through pipes. MCP tool calls do not.
# CLI: pipe-native, scriptable, cron-able
telegram read --name "Robin" --limit 20 \
| jq -r '.[].text' \
| grep -i "sprint" \
| sort -u
That pipeline isn't expressible as a single MCP tool call — it'd be three: read the messages, filter the JSON, dedupe. Each round-trip carries the result back into Claude's context. The pipeline above carries nothing back into context; the agent only sees the final filtered output. Composability with shell isn't a footnote — it's a different category of agent-side behaviour.
The composability gets sharper across CLIs. The GIF at the top of this post is one example — answer "how many people do I have in both Telegram and Signal?" with a single pipeline:
{ telegram list --json | jq -r '.[].name' | sort -u
signal list --json | jq -r '.[].name' | sort -u
} | sort | uniq -d | wc -l
Concatenate both name lists in a brace group; sort | uniq -d emits only the names that appeared in both. The same shape works across any pair of CLIs that return JSON: workspaces across two self-hosted Kan instances, document titles across two Outline wikis, senders across Gmail and Telegram. No single MCP tool answers any of those questions. With CLIs and shell, each is one pipeline.
2. Output control
You decide what your CLI prints. --json for jq-pipe ergonomics. --raw for grep-friendliness. | head -5 for short responses. The agent shapes the output before it ever returns to context.
MCP tool returns are whatever the tool author chose. If the Gmail MCP returns 80 fields per message and you only wanted three, those 80 fields land in context anyway. Over many calls in a session this adds up — and you can't trim it without forking the MCP.
3. No per-project plugin toggle
Project A wants the kan MCP enabled, Project B doesn't, Project C is somewhere new where you haven't decided. Every MCP server is one more flag on the per-project enable/disable matrix, one more thing to remember when you bootstrap a new repo.
A CLI documented in your global CLAUDE.md is just there. Same six CLIs across all 47 of my projects. Zero configuration drift.
Where MCP still wins
I am not arguing against MCP. There are real cases where it's the right answer:
- Tools the agent should always be able to reach for, where the cost of "discover via
--help" is too high to pay each session. Anthropic's hosted MCPs for in-house features fit this — you don't want the agent to learn the contract every turn. - Complex typed parameters where the schema's value as documentation exceeds its cost. A "create a calendar event" call with 12 enum-typed fields is genuinely easier in MCP than CLI.
- Streaming or long-running tools where stdout buffering isn't enough.
- Tools running in environments where the agent doesn't have shell access (rarer than people think — Claude Code, the SDK, and most agent harnesses do have shell). When shell is unavailable, MCP is the only answer.
For my stack, none of the chat services fit any of those categories. Each is a low-frequency, simple-parameters, fits-in-a-pipe task. CLI is the cleaner shape.
The CLAUDE.md pattern
After today's trim, the minimum-viable CLI bullet has three jobs:
- Where the tool lives.
~/.local/bin/gmail - How it auths.
source ~/.claude/.envfirst. - The policy gotcha. Multi-instance flag. Auth-pattern-mismatch. Don't-do-this footgun.
Everything else, <tool> --help can teach. A 250-char bullet steers Claude correctly for the high-frequency use cases and lets discovery handle the long tail. You get the cost-control knob that MCP doesn't give you.
The shape
Eighteen months of agent tooling has been MCP-shaped. The default question has been "is there an MCP for this?" and the default answer has been to attach one. For the kinds of tasks I do — chat across personal accounts, edits across self-hosted wikis, kanban updates, file moves — that default has been quietly wrong, and the swap has been quietly worth it.
The post-MCP CLI isn't a regression to the bad old days. It's the discipline of describing a tool just well enough for the agent to use it, no more — and treating the tool itself as a Unix citizen, with stdout, pipes, exit codes, and composability with the four decades of shell tooling that already exists.
If you're attaching your fifth MCP this week, ask whether a CLI would do the same job. Increasingly often, it would.
The CLIs in this post are personal tools and live in ~/.claude/cli-tools/. The patterns generalise — most of them are 100–500 lines of JavaScript or Python with an .env-sourced credential and a --help that's honest about what it does. See also Multi-Instance CLIs for Self-Hosted Services for what the CLAUDE.md bullet ends up looking like when one CLI handles multiple deployments.