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 total43,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.

Tokens per task read, averaged over 300 Asana tasks.

Estimated tokens, averaged per task:

data shapenskill (md)get_taskget_storiesMCP totalsavedfactor
Light579709,3301,64010,97091%11.3×
Field-rich291,3309,5301,82011,35088%8.5×
Chatty372,1809,9404,29014,22085%6.5×
Field-rich and chatty1772,77013,3205,08018,40085%6.7×
All 3003002,21011,7804,02015,79086%7.1×

Exact bytes, same buckets:

data shapenskill (md)get_taskget_storiesMCP totalsavedfactor
Light573,78930,7935,42236,21590%9.6×
Field-rich295,18431,4316,01937,45086%7.2×
Chatty378,51032,78914,14246,93282%5.5×
Field-rich and chatty17710,78343,95016,77160,72182%5.6×
All 3003008,63338,86413,25152,11583%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.