I re-listened to Kelsey Hightower's Zero Token Architecture talk from PlatformCon this week. On first listen, back in June, it registered as a cost argument: stop burning tokens on things that don't need a model. On second listen a different thing stuck, the idea he kept returning to in one form or another. Infer once. Export the logic. Run it without further inference.

That is a description of a well-built MCP server. It is also, in reverse, a description of most of the MCP servers I have seen, and a few I have built.

One thing to settle before going further, because the name invites the wrong reading. ZTA is not anti-LLM. The model is still the only component in the system that can take "find the vendor invoices stuck in approval and tell me why" and decide what to do about it. Nothing deterministic does that. What ZTA is against is having no architecture around the model, so that tokens end up doing the job a design should have done. Every time an agent reasons its way through a problem you have already solved, that is not intelligence at work. That is a missing export.

This piece is about what that missing export looks like in MCP, because it turns out there are two of them.

01What ZTA actually says

Three verbs.

Infer

Work it out

Use the model to work through a problem you do not yet understand, where the shape of the answer is unknown.

Export

Capture it as a thing

Once you understand it, capture the answer as something that exists outside the model — code, config, a binary, a library, a schema.

Run

Execute without the model

Execute the export without going back to the model. Software does the repetition.

Kelsey's own example is deliberately unglamorous. A company receives customer CSV files in every format imaginable and points an agent at them. After enough runs the team notices that five formats cover almost everything. So they write a plain importer for those five and send only the unknown ones to the agent. The model did the discovery. Software does the repetition. The agent is still there, but it is now a consultant brought in for new problems, not a permanent component in the hot path.

Greg Herlein put the economics well in a follow-up post: tokens should be NRE, not COGS. In hardware, non-recurring engineering is the expensive one-time work that produces a reusable asset, a jig, a mask, a library. Cost of goods sold is what you pay per unit, forever. Software's whole advantage is zero marginal cost per copy. Spending inference on every request to regenerate a solution you already have turns that advantage back into a bill of materials.

Look at MCP through that lens and the pattern applies twice. Once at runtime, where the export is the tool. Once at build time, where the export is the SDK.

02Where the tokens actually go in an MCP call

Before the two layers, it helps to be precise about what a tool call costs, because "zero token" is a phrase that invites overclaiming, and I would rather the argument survive contact with someone who has looked at their own usage dashboard.

Every MCP tool call spends tokens in four places.

01 · SCHEMA

Sits in context

The tool's definition is in the context window on every turn, whether or not the tool is used.

generic forty endpoint schemas
curated a handful, tighter
02 · CALL

Output tokens

The model constructs the call: picks the tool, fills the arguments.

generic a free-text query to write
curated two typed fields
03 · RESULT

Input tokens

The result comes back into context and the model has to read all of it.

generic raw upstream JSON
curated shaped, derived fields done
04 · RETRY

The multiplier

Wrong, incomplete, or badly shaped result: another round of reasoning, another call.

generic × N, every time
curated × 0

Curated tools shrink 01 to 03 and remove 04. That fourth box is where re-inference of solved logic lives.

Curated tools do not eliminate any of those four. The schema is still in context, the call is still inference, the result still comes back. What they change is the size of each and the count of the fourth. A tool that returns exactly the shape the model needs means a smaller result and no retry. A tool whose arguments are two typed fields instead of a free-text query means a shorter, more reliable call. Fewer tokens per outcome, not zero tokens.

What they do eliminate entirely is re-inference of solved logic. That is the ZTA claim, and it is the one worth making.

03Runtime: the tool is the export

In an MCP-backed agent the model's job is to understand the intent and pick the right tool with the right arguments. The tool's job is everything else. Whether that division actually holds depends entirely on how the tool is designed, and there are two ways to design it.

A generic tool hands the model a capability and leaves the thinking to it. execute_sql(query). http_get(url). run_command(cmd). The tool does exactly what it says and nothing more. Everything above it, working out which tables, which joins, which columns, what a good result looks like, is the model's problem, and it is the model's problem again on the next call, and the one after that. The reasoning never leaves the context window, so it never becomes an asset. You are paying for the same discovery on every invocation.

A curated tool has already done the reasoning. get_stuck_invoices(older_than_days, approver). Someone, or the agent itself, once, in a discovery environment, worked out which tables hold invoices, which join gets you the approval chain, which status codes actually mean "stuck" as opposed to merely "pending", and what shape the answer needs to be in for the model to finish the job without a second look. All of that is baked into the backend as a deterministic function. The model reads a short schema, supplies two arguments, and gets back exactly what it needs.

"Curated" is doing real work in that sentence, so here is what it means concretely. The arguments are typed and named for the intent, not the implementation: older_than_days, not where_clause. The result is shaped for consumption, not dumped: the fields the model will actually reason about, in an order that reads, with anything derived (days stuck, next approver) already computed. Errors are semantic: "approver not found" rather than a stack trace the model then has to interpret. And the description tells the model when to use it, not how it works, because the how is no longer the model's concern.

None of this is new. It is the difference between an API and a good API, applied to a consumer that pays per token to read your response.

04The uncomfortable part

Most MCP servers are API wrappers with a nicer schema.

I mean that literally. One tool per endpoint. Parameters passed straight through. Raw JSON back, with every field the upstream system happened to return. The server author took the OpenAPI spec, generated tool definitions, and shipped. That is the generic-tool pattern with a protocol around it, and the protocol does not fix it. The model is still doing the reasoning about which of the forty tools to call, in what order, with what identifiers pulled out of which previous response.

MCP gives you the seam where deterministic logic belongs. It does not force you to put any there. When a team says "we have MCP" and is still watching token spend climb on every run, this is usually why: they built the seam and left it empty.

05Build time: the SDK is the export

The runtime argument is the one people can see, because it shows up in the usage dashboard. The build-time version is quieter. Herlein names it in general terms: agents hand-roll identical solutions at every call site, paying inference each time instead of building the shared abstraction. In MCP it has a very specific shape, and it is where I have personally wasted the most.

Every MCP server that sits in front of an enterprise system has the same cross-cutting work to do before a single tool runs. Take the incoming token and validate it. Extract the subject. Propagate that identity downstream, so the source system enforces its own authorisation instead of trusting the server's word for it. Decide what is safe to cache and for how long, usually the length of the session. Emit an audit record that says who called what, with which arguments, and what came back. Register the tool manifest somewhere a registry can see it.

None of that is specific to the system behind the server. The Ariba server needs it. The ServiceNow server needs it. The internal ticketing server needs it. It is the same problem every time.

In my earlier servers, every one of these was implemented fresh. Not because anyone decided it should be. Each server was built by a coding agent working from a spec, and each time the agent solved the problem from scratch, competently, and differently. The sub extraction lived inline in the handler in one server, in a middleware layer in the second, in a helper module in the third. One cached the resolved identity per session, one per request, one did not cache at all. Downstream propagation was a forwarded header in two of them and a token exchange in the third.

Three servers. Three working solutions to the same problem. All slightly different. When one of them needed a fix, an edge case in token validation, it was a fix I had to rediscover three times, in three codebases, in three shapes.

That is the ZTA loop run three times when it should have run once. Every one of those coding-agent sessions was inference spent re-deriving an answer that already existed in the server next door. The export is an SDK, and the shape of it is roughly this:

from mcp_platform import GovernedServer, Identity, oidc, token_exchange

server = GovernedServer(
    name="procurement",
    auth=oidc(issuer=ISSUER),
    downstream=token_exchange(audience="procurement-api"),
)

@server.tool()
def get_stuck_invoices(
    ctx: Identity,
    older_than_days: int,
    approver: str | None = None,
):
    # ctx.sub, ctx.downstream_token and ctx.session are already resolved.
    # Audit and manifest registration happen in the framework, not here.
    return procurement.query_stuck(ctx.downstream_token, older_than_days, approver)

The line to notice is the division of labour. The server author writes the tool: the arguments, the backend call, the result shape. That is the part that is specific to this system and this intent, and it is the part worth a human's attention. Identity, propagation, session caching, audit, and manifest registration are the framework's problem, solved once and fixed once.

The coding agent still builds the server. That is not the part to give up. It just builds on top of the export instead of re-deriving the export, which means its spec is now three lines about the tool rather than a page about OAuth, and the surface where it can invent something subtly wrong is a lot smaller.

06When to run the loop again

ZTA does not say never infer. It says know when you are inferring, and make it a decision rather than a default. For MCP that turns into a lifecycle with four stages.

  • 01DiscoveryThe agent gets generic tools, in a non-production environment, against non-production data. execute_sql, http_get, whatever it needs to explore. It constructs queries, fails, retries, finds what works. You watch what it does repeatedly. This is the CSV company noticing that five formats cover almost everything. It is also the only stage where generic tools belong.
  • 02ExportThe recurring patterns get hardened into curated tools with proper schemas, semantic errors, and shaped results. The tool manifest is pinned at registration, so the thing that was reviewed is the thing that ships.
  • 03RunProduction gets the curated tools only. The generic ones do not ship. If the model needs something that is not there, that is a signal to go back to discovery, not a reason to hand production a SQL prompt.
  • 04ChangeWhen the source system changes, or the intent changes, the manifest changes with it. The deployment gate compares the new manifest against the pinned one and blocks the rollout until someone has looked. Then you run discovery again, for that one tool, and export again.

This is also where governance stops looking like bureaucracy. The registry, manifest pinning, and CI quality gates are not overhead on top of the architecture. They are the mechanism that decides which side of the infer/run line a tool sits on, and that catches it when a tool drifts across without anyone deciding it should.

07Where this breaks

Over-curate and you get tool sprawl. Hundreds of narrow tools, a context window full of schemas, and a model that cannot find the right one among them, which brings back the retry loop you were trying to remove, now at the tool-selection layer instead of the query layer. The long tail of intents still needs the model's flexibility. The point is not to pay inference for the head of the distribution, and to be honest about where the head ends.

Harden too early and you export the wrong thing. The CSV company waited until five formats were clearly recurring. If they had built the importer after the first file, they would have built it for one format and rebuilt it four times. Discovery has to run long enough to be sure what is actually recurring.

And everything above applies to skills and system prompts as much as it does to tools. They get regenerated, re-pasted, and re-derived every time, and almost never exported into something versioned and shared. That is the same problem one layer up, and it is a separate post.

08Which side of the line is your tool on

A short test I now apply to any tool before it ships to production.

Four questions

  • Does the model have to work out how to use it, or only when?
  • Does the result come back in the shape the model will reason about, or in the shape the upstream system happened to return?
  • Would a second call with the same intent produce the same tool call, or does the model re-derive it each time?
  • Is the auth in this server code the same auth as in the last server, or a new solution to the same problem?

If the answers are "how", "upstream", "re-derives", and "new", the tool is still in the infer column. It works. It just costs you every time.

09Turning the test into a gate

Four questions on a checklist are still a person inferring, one tool at a time, usually the week before a release. So the next thing I am building is an inspector: a step in the pipeline that scores an MCP server against the line above and fails the build when a tool sits on the wrong side of it.

inputTool manifestschemas, descriptions, result shapes, plus the server's auth code
once per tool versionInspectorseven checks, most deterministic, two model-judged
exportScore pinned to manifestthe verdict travels with the version that was reviewed
change gateShip, or back to discoveryproduction never runs the judge
CheckWhat it catchesKind
Argument names vs implementation smellsquery, sql, where_clause, payload: the model is being handed the howlint
Result schema size and derived fieldsRaw upstream dumps; fields the model will have to compute itselflint
Description says when, not howDescriptions that explain internals instead of intentmodel judge
Result shaped for the model, or merely dumpedTechnically complete results the model still has to interpretmodel judge
Tool-call stabilityReplay the same intent a dozen times; a curated tool converges to one call, a generic tool does notreplay
Retry rate per toolBox 04 above, measured from the traces you already havetraces
Auth, propagation, audit diffed across serversA third bespoke sub extractor landing without anyone seeing itlint

Five of seven need no model at all. Two do. Which makes this an eval harness for Zero Token Architecture that is not, itself, zero token. Sit with that for a second; I had to.

Two of those checks need a model. Whether a description says when rather than how is a judgement call, and so is whether a result is shaped for the model or merely dumped. So yes: I am proposing to spend tokens on a judge whose job is to catch tools that spend too many tokens, and I wrote eight sections arguing against exactly that kind of thing before getting here. In my defence, it is the ZTA loop applied to itself. Infer at review time, once per tool version, and export the verdict into the manifest as a pinned score. Production never runs the judge. The judge runs when the tool changes, which is exactly when a decision is being made. If that still sounds like a rationalisation, the deterministic five will happily ship without it.

Whether that ends up as a checklist that grew a CI step or an eval harness in its own right, I will find out by building it. It is the natural next stage of the lifecycle above: the Change gate stops asking "did someone look?" and starts asking "which column is it in?"

10Fundamentals

Kelsey's one-line abstract for the talk was that the fundamentals still matter and burning tokens is not a requirement. The fundamental in MCP is knowing which part of the job is intent and which part is just work. Intent goes to the model, because nothing else can do it. Work gets exported, into a tool at runtime and into an SDK at build time, and runs without asking again.

References

  1. Kelsey Hightower, "ZTA: Zero Token Architecture", PlatformCon 2026, NYC Live Day, 25 June 2026 — session, recording.
  2. Greg Herlein, "Tokens Should be NRE, Not COGS".