Tool count explosion
32 endpoints become 32 tools. Selection accuracy degrades sharply past ~15–20 tools — every definition competes for the model's attention on every single turn.
Field guide · Rev A · Shaping APIs for agents
Your API speaks REST — resources, verbs, cursors, status codes. Agents speak intent. Everything interesting about the Model Context Protocol lives in the translation between the two. This is a field guide to that translation: where naive conversion breaks, and four ways to do it properly.
Point a generator at an OpenAPI spec, mint one tool per endpoint, ship it. The result compiles but doesn't work: agents pick wrong tools, drown their own context, and fumble multi-call chains. Four structural reasons why.
32 endpoints become 32 tools. Selection accuracy degrades sharply past ~15–20 tools — every definition competes for the model's attention on every single turn.
The user wants "order dinner." The API offers POST /cart/items then POST /orders. The agent must plan a four-call chain, thread IDs through it, and not lose state halfway.
getOrderTracking describes a route, not a reason. Agents choose tools by name and description — REST semantics carry almost no signal about when to call.
OpenAPI summary fields are terse, missing, or assume a developer reading in context. No examples, no "use when / don't use when." Agents fly blind.
Every tool definition is paid for in context tokens and attention on every turn — before the agent does anything. Past a budget of roughly fifteen tools, selection errors and token burn compound each other.
So the design question inverts. Not "how do I expose my API?" but "what is the smallest set of tools that covers real intents?"
Fig. 01 — schematic, illustrative curve
Even before composing intents, a single endpoint needs surgery. Watch the same operation cross the shaping band — the highlight cycles through each transformation.
GET /restaurants:
operationId: listRestaurants
summary: List restaurants.
parameters:
- lat # number, required
- lng # number, required
- cuisine # string enum, 41 values
- page_cursor # opaque string
- include_meta # boolean
responses:
200: RestaurantList
# 47 fields · ~52 KB typical
{
"name": "search_restaurants",
"description": "Find restaurants near a place.
Use when the user names a craving or an
area. Not for reorders → reorder_last().",
"input": {
"near": "string — address or place name",
"craving": "string — cuisine or dish, optional"
},
"returns": "top 5 · name, rating, eta,
price_band // ~0.4 KB
"guards": "read · safe · no approval"
}
Intent naming. Rename from route-derived identifiers to verb-first intent names, and attach a safety classification the runtime can enforce.
A tool response isn't data delivery — it's a deposit into the agent's working memory, re-read on every turn that follows. APIs return everything a UI might render someday. An agent needs exactly enough to choose its next action. Shape for the decision, not the data.
tokens the agent needed vs tokens it was sent — to scale
Responses persist. By the last step of a six-call task the agent is re-reading every earlier payload on every turn — ~78,000 tokens of JSON carried before it decides anything. Shaped: ~600.
And it's not just cost. Attention is finite: the answer to "which restaurant?" is one line drowning under forty-six the agent will never use. Payload noise is selection error.
Every tool carries a response contract — an architect-editable allowlist. What isn't listed doesn't ship. The response schema becomes a design surface, not an accident of the backend.
Distill for decisions: min/max delivery fees collapse into price_band; fifty review excerpts into one rating. The derived field is usually the only one the agent ever needed.
Cap every list. Return more_available: true instead of page eleven. The agent asks for more only when the task genuinely demands it — which is almost never.
Opaque IDs travel with their human names — r-001 · "Mission Chinese" — so the model chains identifiers it can anchor instead of hallucinating ones it can't.
Two tools, two costs. The cheap one answers most turns; the expensive one exists precisely so the cheap one can stay cheap. Never merge them back.
The unshaped payload lives on as an MCP resource — reachable for audits, debugging, and distrustful architects, but never in the agent's loop by default.
A response should contain exactly what the agent needs to choose its next action — plus a path to more. Everything else is a tax collected on every turn that follows.
The same fifteen, mapped onto the request path. Shape gaps live where tool definitions meet the agent. Behavior gaps live on the wire at runtime. Trust gaps are the plane underneath — they span every hop. Select a marker, or let it walk the path.
Four architectures for turning N endpoints into a toolset an agent can actually drive. They're not rivals — they're layers. But one of them has to lead.
Select a subset, rename, rewrite descriptions, simplify schemas. One endpoint = one tool.
Transparent — architects can audit the mapping.
Chattiness survives; caps at ~15–20 tools.
Fit — small, read-heavy APIs
One tool per capability with an operation discriminator. Six tools for a 32-endpoint API.
Tiny tool count; scales to 500-endpoint estates.
Fat union schemas confuse mid-tier models; per-op safety gets awkward.
Fit — auto-fallback for huge APIs
Compile task-level tools that orchestrate multiple endpoints server-side. The agent makes one call; IDs and state never leave the runtime.
Best task success; biggest token savings; safety attaches at the intent — how humans reason about risk.
Needs an orchestration runtime: partial failure, compensation, timeouts. Composites must be eval-tested.
Fit — transactional APIs · the default publish
Three meta-tools — search, describe, invoke. The agent discovers what it needs, when it needs it.
Unbounded API size at near-zero context cost; invoke is a single governance chokepoint.
Extra round trips; weaker models fumble discovery; undramatic in demos.
Fit — the long tail behind the intents
| Axis | A · Curated | B · Facade | C · Intents ★ | D · Dynamic |
|---|---|---|---|---|
| Agent task success | ● | ● | ●●● | ●● |
| Token efficiency | ● | ●● | ●●● | ●●● |
| Scales to huge APIs | ✗ | ●● | ●● | ●●● |
| Auditability | ●●● | ● | ●● | ●● |
| Governance attach point | per tool | per capability | per intent ★ | per invoke ★ |
| Build cost | low | low | high | medium |
The directions compose. Publish a small set of compiled intents as the face of the server, keep every primitive reachable through meta-tools, and leave a flat curated mode for architects who want to see every wire.
5–8 task-level tools — order_food, track_and_resolve — orchestrated server-side. Each one is a testable task and a governable unit: place_order requires approval is exactly how risk owners think.
search_tools / describe / invoke expose every curated primitive at near-zero context cost. Invoke doubles as the audit and policy chokepoint.
One switch flips the server to transparent 1:1 tools. A trust feature for skeptical architects — cheap to keep, disproportionately reassuring.
Curated tools with naming, schema diet, response shaping, guidance. Mostly built.
Unlocks large-API prospects immediately; establishes the invoke chokepoint governance will use.
Orchestration with failure semantics — behind a beta flag, gated on eval pass rates.
*Unless real APIs prove too large even for curation + discovery. Don't build it first.
An intent is a testable task — "order a meal under $30" either passes or fails. Compiled intents plus auto-generated evals produce the one trust metric nobody else can print: 12/12 agent tasks pass.
MCP ships four primitives; almost every converter uses one. Classifying each capability into the right primitive — not just tool-or-skip — is instant, visible sophistication.
Reserved for operations that do something. Every tool costs context on every turn — spend the budget on verbs, not lookups.
Menus, promo lists, docs — served as resources, not read tools. The client caches them: get_menu stops costing a round trip.
Drafted workflows double as MCP prompts — "file a delivery complaint" becomes a pre-parameterized entry the user picks, not a chain the agent guesses.
Mid-workflow approvals and OTP steps without breaking the run. This is how place_order approval should actually work — a pause, not a dead end.