hankdoes.ai

AI experiments and musings.

MCP vs CLI: The Token Tax

For local agentic workflows, one developer, one machine, iterating fast, CLI tools composed with unix pipes are leaner on tokens, faster to iterate, and more expressive than MCP. The model already knows the unix toolchain. You don’t need to implement, schema-describe, and pay tokens for capabilities that jq and grep give you for free. MCP’s strengths are organizational. If you need centralized auth, instant distribution, black-boxed implementations, credential isolation, long-running processes, or statefulness, use MCP. If you’re building local automation and talking to your own machine, you’re paying a token tax for infrastructure you don’t need.

Every time someone tells me I should wrap my tools in MCP servers, I run the numbers and arrive at the same conclusion. Not always. But more often than people think.


The Case Against MCP (For Local Work)


The Token Tax

I ran every example below through the Anthropic token counting API. The scenario is simple, a tool that returns your followers list. Each follower has a username, display name, country, age, follower count, and join date. 20 users.

Step 1: Format

Both tools return the same data. The difference is that MCP returns JSON because that’s the protocol. A CLI tool can return whatever is cheapest for the LLM to read.

Here’s the JSON that MCP returns (truncated to 3 of 20):

[
  {
    "username": "alexchen",
    "display_name": "Alex Chen",
    "country": "GB",
    "age": 41,
    "followers": 12767,
    "joined": "2021-03-15"
  },
  {
    "username": "jordanmueller",
    "display_name": "Jordan Mueller",
    "country": "US",
    "age": 29,
    "followers": 18626,
    "joined": "2023-02-23"
  },
  ...
]

Here’s the same data from a CLI tool:

username          display_name       country  age  followers  joined
alexchen          Alex Chen          GB       41   12767      2021-03-15
jordanmueller     Jordan Mueller     US       29   18626      2023-02-23
samsilva          Sam Silva          BR       51   22246      2019-08-11
taylortanaka      Taylor Tanaka      FR       43   1447       2022-09-01
morgankim         Morgan Kim         FR       45   6726       2023-09-03
caseypatel        Casey Patel        BR       47   17045      2021-10-15
rileywright       Riley Wright       GB       21   11765      2020-04-23
jamiegarcia       Jamie Garcia       JP       28   24544      2025-10-06
averyschmidt      Avery Schmidt      GB       50   21686      2020-01-04
quinntakahashi    Quinn Takahashi    KR       19   5659       2025-11-27
drewsantos        Drew Santos        US       44   2709       2024-11-03
blakemartin       Blake Martin       US       27   10201      2018-07-11
sagelee           Sage Lee           IN       26   17200      2024-04-26
charliejohnson    Charlie Johnson    US       21   21035      2021-08-09
reesepark         Reese Park         US       52   13290      2020-06-23
haydenwilliams    Hayden Williams    DE       48   1879       2020-12-28
finleynakamura    Finley Nakamura    DE       27   19974      2024-02-04
rowanoliveira     Rowan Oliveira     DE       25   4757       2022-03-06
emerybrown        Emery Brown        CA       44   4818       2021-09-11
dakotasingh       Dakota Singh       JP       49   7714       2022-02-28
Format Tokens
JSON 1,357
Compact text 487

Same data, same information content. 64% fewer tokens just from dropping the repeated key names, quotes, braces, and indentation. The LLM reads both formats just fine.

Step 2: Filtering

“But that’s not fair!” you cry out, “if you needed it to be machine parsable too you would have to use something like JSON”.

First of all, no, because any of these TSV, CSV, TOON formats are machine parsable all the same.

Now say you ask “show me my followers from Germany.” Both tools output JSON this time, same format. The difference is that CLI can pipe through jq before the result enters context.

# CLI
list-followers hank | jq '[.[] | select(.country=="DE")]'

The LLM sees:

[
  {
    "username": "haydenwilliams",
    "display_name": "Hayden Williams",
    "country": "DE",
    "age": 48,
    "followers": 1879,
    "joined": "2020-12-28"
  },
  {
    "username": "finleynakamura",
    "display_name": "Finley Nakamura",
    "country": "DE",
    "age": 27,
    "followers": 19974,
    "joined": "2024-02-04"
  },
  {
    "username": "rowanoliveira",
    "display_name": "Rowan Oliveira",
    "country": "DE",
    "age": 25,
    "followers": 4757,
    "joined": "2022-03-06"
  }
]
  Tokens
MCP (all 20 users returned) 1,357
CLI + jq (3 users returned) 223

With MCP, the full payload travels through the protocol into context and the LLM has to scan all 20 records to find the 3 it cares about. This burns credits and blows out your context window. With CLI, jq does the filtering in the shell and only the 3 relevant records enter context.

The tool implementation logic is identical in both cases, they both return all followers as JSON. The CLI version gets filtering for free from jq.

Filtering by country == "DE" is not a semantic operation. It’s string matching. Axiom 1, LLMs should only do what only LLMs can do, and scanning 20 JSON objects for a field value is not one of those things.

Step 3: The Schema Tax

“Ok fine, I’ll just add filtering to my MCP tool.”

You can. But now you’re paying for it twice.

Now say you want “how many followers per country?” With CLI you can answer this a few different ways:

# jq aggregation
list-followers hank | jq 'group_by(.country) | map({country: .[0].country, count: length}) | sort_by(-.count)'

# or just classic unix pipes
list-followers hank | jq -r '.[].country' | sort | uniq -c | sort -rn

# or if you just want one country
list-followers hank | jq -r '.[].country' | grep -c DE

That last one returns 3. A single character. The LLM asked a counting question and got a number back instead of 1,357 tokens of JSON to reason over.

The model already knows jq, grep, sort, uniq, wc. It can compose them however it wants across every CLI tool you write, with zero implementation work from you.

For MCP to match this, the tool author has to implement filtering logic in code and add parameters to the tool schema. Even being generous and using a single filter param that accepts expressions like country=DE instead of a dedicated param per column, plus limit, fields, and group_by, the schema goes from 98 tokens (basic) to 299 tokens. That’s 201 extra tokens in the schema, paid on every single turn whether the tool is used or not. Plus the tool author has to write and maintain a filter expression parser.

And that’s one tool. Ten tools with the same pattern and MCP is paying ~2,000 extra tokens per turn in schema overhead alone, plus all the implementation and maintenance burden. CLI still just has jq.


When MCP Wins

MCP does have real advantages that local CLI tools can’t replicate.