DEV Community

Gérald Pereira
Gérald Pereira

Posted on Originally published at blog.octoperf.com

Designing a token-efficient MCP server: 5 patterns from shipping one with ~100 tools

Designing a token-efficient MCP server: 5 patterns from shipping one with ~100 tools

Here is the thing nobody tells you when you start writing a Model Context Protocol server: the hard part is not exposing your API to an LLM. The hard part is not exposing too much of it.

We just shipped an MCP server at OctoPerf that drives a full load-testing platform, around 100 tools, and load testing turns out to be a brutal stress test for token budgets. A single virtual user is a deeply nested action tree. A test run produces gigabytes of results. One captured HTTP response body can be larger than the entire context window. If you naively mirror a REST API into MCP tools, you get an agent that is expensive, slow, and confused.

These are the five patterns we used to avoid that, plus the auth setup that makes it usable without an API key. None of it is specific to load testing. If you are building your own server, you should be able to lift these directly.

Why the context window is the real budget

A REST API and an MCP server look almost identical: both move JSON over HTTP. But they feed very different consumers. A web UI fetches a 200 KB entity, renders the 2% it needs, and throws the rest away for free. An LLM cannot throw anything away. Everything a tool returns is read, token by token, on every reasoning step that follows.

That hits you three ways:

  • Money. Tool results are input tokens, and in an agentic loop they get re-read every turn. One oversized response is billed many times.
  • Latency. Bigger context means slower responses, and an agent chains dozens of calls per task.
  • Quality. This is the sneaky one. A model digging through 50,000 tokens of irrelevant JSON is measurably worse at finding the one field that matters.

So we made token cost a first-class design constraint up front, and it shaped the whole API surface.

Auth first: OAuth 2.1, no API key

Before the patterns, the part people ask about most. There is no API key anywhere. Auth is OAuth 2.1 with PKCE and Dynamic Client Registration, over Streamable HTTP. DCR means a client self-registers instead of you minting credentials by hand, and every tool call runs as the connected user with their own permissions, revocable at any time. We did not want yet another long-lived secret pasted into a config file, and an MCP server full of write tools is exactly where you do not want one.

Pattern 1: presigned URLs, keep the bytes out of the conversation

Ship the file to its destination, not through the messenger

Load testing is file-heavy: CSV datasets go up, JTL results, HAR archives, Playwright traces and PDFs come down. Pushing file content through tool results would be absurd. A modest 2 MB results file is roughly half a million tokens, spent on bytes the model would mostly never read.

So a file tool never returns content. It returns instructions to fetch it: a presigned URL with a single-use, short-lived token, the HTTP method, and an expiry. The agent host's code interpreter (or a plain curl) pulls the bytes straight from our REST API, and the model only ever sees the few dozen tokens of the envelope.

The payoff goes past cost. Because the bytes bypass the model, file size stops being the LLM's problem. The agent can pull a 50 MB Playwright trace, unzip it locally, and grep the one failing selector, something no context window could absorb.

Pattern 2: return listings, not entities

Files are the obvious offender. Entities are the insidious one. In our REST API a virtual user carries its full recursive children action tree, which for a recorded checkout easily runs to thousands of lines of JSON. An agent calling list_virtual_users to find an id needs none of it.

So every list, create, and import tool returns a compact projection instead: id, name, description, tags, timestamps, and a url. Keep what the agent reasons about (the id to chain into the next call, the human-readable name), drop what it does not (the tree, ids it already knows, internal discriminators). We have fifteen of these, one per entity family.

That last field matters more than it looks. Every listing carries a deep-link to the matching page in the UI. It costs a handful of tokens and buys two things: the agent can hand the user a clickable link in any summary, and it gives the agent a graceful exit. When a question is better answered by an interactive chart than another round of tool calls, it can just point you at the page instead of burning tokens reproducing it.

Ballpark: a full entity weighs tens of thousands of tokens, its listing about sixty. On a busy project that is two orders of magnitude, on the very first call.

Pattern 3: patch, don't replace

Reading was the easy half. Editing is where a naive design really bleeds. If the only write tool is update(fullEntity), then renaming one action in a 500-action tree forces the agent to read the whole tree, regenerate it with one field changed, and send it all back. Two full copies through the context, plus a real risk the model mangles a field it should not have touched.

Instead every entity family gets a patch_* tool built on RFC 6902 JSON Patch. The agent sends only the operations:

[
  { "op": "replace", "path": "/children/3/name", "value": "Submit payment" },
  { "op": "add", "path": "/children/7/enabled", "value": true }
]
Enter fullscreen mode Exit fullscreen mode

A surgical edit costs a few dozen tokens no matter how big the entity is. Server-side, the patch is applied and then re-deserialized through Jackson before persisting, a round-trip that rejects any patch producing a structurally invalid entity. The agent can be wrong, but it cannot corrupt your script.

One catch: to write a correct patch against a polymorphic tree, the model needs the shape of every node type. Guessing burns tokens on failed attempts. So we publish the entity schemas as MCP resources (JSON Schema 2020-12, one oneOf branch per subtype), with a plain-HTTP fallback for clients that do not read resources. The agent loads a schema once, on demand, instead of rediscovering field names by trial and error, and a failed patch points back at the relevant schema so the retry is usually one shot.

Pattern 4: layered reads, index before detail before body

Read the index first, drill into one representative second

The patterns above are generic. This one is about shaping a workflow. When we validate a script we replay it and capture, for every action, four HTTP entities: the request as recorded, the request as replayed, and both responses. For a 24-action journey that is easily megabytes. The one thing the server must not do is hand all of it over at once.

So the validation API is layered on purpose:

  1. An index call returns one tiny entry per action: success and failure counts, timestamps, no bodies. A few hundred tokens, usually enough to group failures by root cause (auth, data, server-side).
  2. A detail call fetches the four HTTP entities for one representative action of a group, the few KB that confirm or refute the diagnosis.
  3. A body call goes one level deeper, retrieving a single body of a single exchange, for the cases where one side fits in context but both would not.

The agent reads the way a good engineer debugs: global picture, then one representative failure, then one specific body if needed. Triaging a red validation costs a few thousand tokens instead of the hundreds of thousands a return-everything design would burn. We applied the same general-to-specific shape to bench reports: there is deliberately no get_full_report tool, just a structure call plus a narrow value tool per widget family.

Pattern 5: skills, not just tools

Tools define what is possible. They do not tell the model what is wise. Faced with 100 tools and a red validation, a model can technically do the right thing, but it will often guess the order of operations and waste calls discovering it.

So we ship skills: markdown playbooks the agent loads on demand, each encoding a complete workflow. Validation triage knows to fix the dominant failure group first and re-validate. Auto-correlation knows to snapshot the entity before rewriting it. Scenario diagnosis knows to read global metrics before drilling into any per-action table. The tools are the API surface, the skills are the reading order, and encoding that order is what keeps the agent cheap and on-rails.

What it adds up to

Take a real workflow: import a HAR, triage a red validation, auto-correlate, re-validate, run a 500-user scenario, diagnose the result. A naive mirror of the REST API would blow through the context window before the midpoint, forcing summarization and losing precision. With these five patterns combined, the same workflow holds comfortably in one conversation, and the context contains almost nothing but signal: listings, indexes, one confirmed failure detail, patch ops, insight verdicts.

If you are building your own MCP server, the patterns to steal are simple:

  • Move bytes out of band. Presigned URLs cost dozens of tokens, files cost millions.
  • Project your entities. Return what the model reasons about, never what your UI renders.
  • Edit by patch, validate server-side, and publish your schemas so the model patches right the first time.
  • Layer your reads. Index before detail, detail before body.
  • Encode the reading order. Tools define what is possible, skills define what is wise.

The full writeup with the actual record types is on our blog, and the server is live if you want to poke at it: docs, source and Claude Code plugin.

If you have shipped an MCP server and found better patterns, I would genuinely like to compare notes in the comments.

Top comments (0)