“Every command should be readable in shell history.”
Why CLI-First?
We used to run MCP servers. Lots of them. Chrome DevTools, Firecrawl, Supabase, custom servers for Gmail and Langfuse. They worked, but they added layers we couldn’t easily inspect.
Then we asked ourselves: Can we read every command our agents execute?
With MCP, the answer was no. Tool calls went through protocol handlers, spawned background processes, and produced results we had to trust. With CLI tools, the answer is yes. Every action is a shell command you can read, replay, and audit.
The Problem with Abstraction Layers
MCP servers solve a real problem—they give Claude access to external systems. But they introduce new ones:
Opaque execution. When Claude calls mcp__supabase__query, what actually happens? You’d need to read the server source code to know. With supabase db execute "SELECT * FROM users", it’s right there.
Persistent processes. MCP servers run continuously. They hold connections, consume memory, and can fail silently. CLI tools run on demand—invoke, execute, done.
Protocol overhead. Every MCP call involves JSON-RPC serialization, tool discovery, and response parsing. CLI tools are direct—stdin, stdout, exit codes.
Trust requirements. You’re trusting the MCP server to do what it says. With CLI, you can verify the exact command before it runs.
Our CLI-First Principles
1. Direct Commands Over Protocol Wrappers
Instead of:
mcp__firecrawl__scrape({ url: "https://example.com" })
We use:
firecrawl scrape https://example.com --format markdown
The command is readable. You can run it yourself. You can pipe it, redirect it, combine it with other tools.
2. Transparent Data Flow
Every CLI command shows you:
- What it’s about to do (the command itself)
- Where data comes from (arguments, env vars)
- Where data goes (stdout, files)
No black boxes. No hidden state.
3. Auditable History
Shell history becomes your audit log:
history | grep supabase
# See every database query your agents ran
Try doing that with MCP tool calls.
4. Context Efficiency
MCP requires protocol negotiation and tool discovery on every session. CLI tools just work—no handshakes, no capability exchanges, no wasted tokens.
The Tools We Use Now
| Task | MCP Server (Before) | CLI Tool (Now) |
|---|---|---|
| Database queries | mcp__supabase__* | supabase db execute |
| Web scraping | mcp__firecrawl__* | firecrawl CLI or curl |
| Browser automation | mcp__chrome-devtools__* | playwright CLI |
| API calls | Custom MCP servers | curl with proper headers |
| File operations | Various MCP tools | Native shell commands |
Practical Examples
Database Access
Before (MCP):
mcp__supabase__query({
query: "SELECT * FROM metrics WHERE date > '2026-01-01'"
})
After (CLI):
supabase db execute \
--project-ref "$SUPABASE_PROJECT" \
"SELECT * FROM metrics WHERE date > '2026-01-01'" \
--output json
Same result. But now you can:
- See the exact query in shell history
- Run it manually to verify
- Pipe output to
jqfor processing - Use standard Unix tools
Web Scraping
Before (MCP):
mcp__firecrawl__scrape({ url: "https://competitor.com/pricing" })
After (CLI):
curl -s https://competitor.com/pricing | \
pandoc -f html -t markdown | \
head -100
Or with Firecrawl CLI:
firecrawl scrape https://competitor.com/pricing --format markdown
Both are transparent. Both are auditable.
Browser Screenshots
Before (MCP):
mcp__chrome-devtools__screenshot({ url: "https://example.com" })
After (CLI):
playwright screenshot https://example.com --output screenshot.png
One command. Clear intent. Verifiable result.
When MCP Still Makes Sense
We’re not dogmatic. MCP has legitimate use cases:
- Complex OAuth flows that need persistent token state
- Real-time subscriptions via WebSockets
- Interactive protocols that require bidirectional communication
For these, use MCP—but audit the server code and understand what it does.
Building CLI Wrappers
When a CLI tool doesn’t exist, we write simple shell scripts:
#!/bin/bash
# tools/langfuse-query.sh
# Direct Langfuse API query - transparent, auditable
set -e
LANGFUSE_HOST="${LANGFUSE_HOST:-https://cloud.langfuse.com}"
curl -sS -X GET "${LANGFUSE_HOST}/api/public/traces" \
-H "Authorization: Bearer ${LANGFUSE_SECRET_KEY}" \
-H "Content-Type: application/json" \
--data-raw "$1"
Usage:
./tools/langfuse-query.sh '{"limit": 10}'
Every bit of this is readable. No hidden logic.
The Security Argument
CLI-first isn’t just about transparency—it’s about security.
Principle of least privilege: CLI tools run with the permissions of the invoking user. MCP servers often run with broader access.
Audit trails: Shell history is automatic. MCP logging requires deliberate implementation.
Code review: A bash script is reviewable by anyone. An MCP server is a full application.
Supply chain: CLI tools from package managers have established trust chains. MCP servers from npm can do anything.
Migration Guide
Moving from MCP to CLI:
- List your MCP servers: What does each one do?
- Find CLI equivalents: Most have them (supabase CLI, gh CLI, aws CLI)
- Write wrapper scripts: For APIs without CLIs, wrap
curl - Update agent prompts: Teach agents to use shell commands
- Monitor and iterate: Some tools work better than others
The Result
Our agents now execute commands you can read:
$ history | tail -20
supabase db execute "SELECT COUNT(*) FROM executions"
gh issue list --repo agents-squads/hq --state open
curl -s https://api.langfuse.com/traces | jq '.data[0]'
playwright screenshot https://agents-squads.com --output og.png
Every action visible. Every command auditable. Every operation transparent.
That’s CLI-first.
This is part of the Engineering AI Agents series—practical patterns for building autonomous AI systems.