Claude Code: a thin script that beat the MCP by ~7x on tokens

I lean on a pile of small skills to run my day, and a lot of them touch our task board (Asana). The obvious way to do that is the Asana MCP server, and it's genuinely good. But the moment I started leaning on it for real work (sprint planning, retros, backlog refinement, any analysis that chews through a whole board), a single session pulls hundreds of tasks through it. At that volume it started to feel heavy. So I measured it, and replaced the hot path with a ~200-line script.

Where an MCP costs you tokens

Two places, and they're easy to miss:

  1. The tool schemas. An MCP server registers every tool it offers, and each tool's JSON schema sits in the context window. The Asana server alone exposes dozens of tools. That's a fixed tax you pay on every request just to have it available.
  2. The response shape. When you fetch a task, the API hands back raw JSON: a gid and a resource_type on every nested object, all custom fields (not just the ones that are set), html_text duplicating the plain text on every comment, and a lot of structural punctuation. You asked for a task; you got a wall of JSON.

For a path I call constantly ("read this ticket in full"), both of those add up fast.

The replacement

A small script that calls the Asana REST API directly and prints clean markdown: title, status, assignee, the custom fields that actually have a value, the description, comments, and an activity timeline. Nothing else.

It's a custom Python CLI wrapped in a skill, and that's the whole trick. The nice part is where the cost goes:

  • The script's code never enters the context window: it runs as a subprocess; only its stdout comes back.
  • The skill that documents it is a few hundred tokens, loaded on demand.
  • The output is compact markdown instead of a verbose JSON tool-result.

The measurement

I ran this on a real, content-heavy ticket with 25 custom fields and 45 stories (comments + activity), and compared the two ways of pulling the same data:

  • Script path: ran the reader → captured the rendered markdown.
  • MCP path: the Asana MCP wraps the same REST API, so I reproduced exactly what get_task + get_stories dump into context. This is the conservative case: I used minified JSON and plain-text comments (not the html_text the MCP also carries), so a real MCP dump would be at least this big.

Bytes are exact; token counts are estimates using Claude-tokenizer ratios (prose ≈3.9 chars/token, punctuation-dense JSON ≈3.3).

bytes ~tokens
asana skill (markdown) 7,853 ~2,000
MCP get_task (JSON) 29,035 ~8,800
MCP get_stories (JSON) 14,250 ~4,300
MCP total 43,285 ~13,100

That's ~82% smaller by bytes, ~85% by tokens (roughly 5.5-6.5×), or about 11,000 tokens saved on a single task read.

Why the gap is so large

The surprise: the dominant cost isn't the data, it's JSON's metadata overhead.

  • Custom fields were 27 KB of the 29 KB task object: ~93% of it. Every one of the 25 fields ships its full definition: each enum field carries all of its options (one field alone returned 23 of them) plus type, gid, and metadata, when the only thing that matters is the one selected display_value. The skill collapses each field to a single line: **Priority:** High.
  • JSON repeats gid / resource_type / resource_subtype on every nested object, and the default task object hauls along followers, hearts, likes, num_likes, and friends. The skill drops all of it.
  • The skill discards disabled/empty fields entirely and renders HTML → markdown instead of carrying raw markup.

So most of what the MCP spends your context budget on is scaffolding you were going to throw away anyway.

This is a heavy ticket, though: lots of fields, lots of comments. The ~6× ratio holds up because the per-field bloat is roughly constant, but a sparse task won't save nearly as much in absolute terms. The heavier your tickets, the more you save.

The same test across 300 tasks

One ticket proves nothing, so I ran the same measurement against a random sample of 300 tasks from that board (2,615 in total, seeded so it reproduces), bucketed by data shape: chatty = 25+ stories, field-rich = 7+ fields set.

Grouped bar chart of tokens per task read across 300 Asana tasks, bucketed by data shape. The MCP get_task + get_stories call costs 11,000-18,400 tokens per read; the markdown skill costs 970-2,770, 6.5-11.3× fewer. Across all 300 tasks the skill averages 2,210 tokens against the MCP's 15,790.

Estimated tokens, averaged per task:

data shape n skill (md) get_task get_stories MCP total saved factor
Light 57 970 9,330 1,640 10,970 91% 11.3×
Field-rich 29 1,330 9,530 1,820 11,350 88% 8.5×
Chatty 37 2,180 9,940 4,290 14,220 85% 6.5×
Field-rich and chatty 177 2,770 13,320 5,080 18,400 85% 6.7×
All 300 300 2,210 11,780 4,020 15,790 86% 7.1×

Exact bytes, same buckets:

data shape n skill (md) get_task get_stories MCP total saved factor
Light 57 3,789 30,793 5,422 36,215 90% 9.6×
Field-rich 29 5,184 31,431 6,019 37,450 86% 7.2×
Chatty 37 8,510 32,789 14,142 46,932 82% 5.5×
Field-rich and chatty 177 10,783 43,950 16,771 60,721 82% 5.6×
All 300 300 8,633 38,864 13,251 52,115 83% 6.0×

The shape of it:

  • get_task barely moves: ~9,000-13,000 tokens whether the ticket is fat or empty. The lightest task in the whole sample still dumped a 25 KB object. It's the custom-field schema again: the API hands back all ~25 of the board's fields with their full option lists no matter what the ticket actually uses. The script keeps the ones with a value. That win shows up on every single read.
  • get_stories is the part that scales (2.5 KB on a quiet ticket, 64 KB on the loudest) because every comment drags a gid, a resource_type, and a full author object behind it. The script folds the same conversation into a fraction of that.
  • The median task has 34 stories and 9 set fields, which lands most of the board in the "field-rich and chatty" row. So the number I actually live with is ~6.7× / 85%, not the 11× the light tickets flash. Light tickets save the biggest fraction (the schema floor dwarfs their content); heavy tickets save the most raw tokens: 15,000+ each.

Add it up across all 300 reads: ~4.7M tokens through the MCP, ~660K through the script. About 4 million tokens saved, 86%. On a board I'm reading all day, that's the difference between a session that fits in the window and one that falls over.

When the MCP is still the right call

This is not "MCPs are bad." The MCP is the better tool for discovery and breadth: find the project named X, search tasks matching Y, who is this user. Those are calls where you can't predict the shape ahead of time and would never want to hand-roll a script for each one. I still use it for exactly that.

The rule I landed on: MCP for exploration, a script for the hot path. The moment a single call becomes something I do many times a day with a known shape, a script I control wins on three things at once: tokens, output shape, and determinism. Best of both: let the MCP discover, let the script fetch.