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.
Two places, and they're easy to miss:
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.
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:
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:
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.
The surprise: the dominant cost isn't the data, it's JSON's metadata overhead.
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.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.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.
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.
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.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.
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.