*By Nikša Perović, 25 Sep 2026. Source: https://niksa.me/agent-tools-design-lessons.* Agents · 9 min read # I built 100+ tools for one MCP server. Here's what actually broke. AI agents don't ask for clarification. They guess, *confidently*, and move on. Design the interface wrong and you won't see it fail. You'll just get quietly wrong answers. I built 115 tools for one MCP server. I thought I was designing tools. It turned out I was designing everything the agent reads. Seven lessons came out of it, and every one comes from watching real usage break. Plain terms an MCP server is a menu of actions an AI agent is allowed to take in your systems. Each action is a *tool*: a name, a short description, and the inputs it expects. The agent reads the menu and decides what to call. Plain terms: on Short explanations for non-engineers, in the dashed yellow boxes. Engineers can switch them off. ## Seven lessons ### 01 Don't ask it to remember the unmemorable If a value has to be character-perfect, an ID or a link, don't trust the model to generate it. Passing one through from a lookup is safe. Authoring one from scratch is where it breaks. Mine produced completely wrong database IDs, with badly built links around them. I only found out by following a broken link and comparing it to what the agent had found in the logs. Plain terms a database ID is 24 random-looking characters. A language model doesn't remember strings like that, it predicts what one should look like. The result looks right and points nowhere. **The fix:** force a lookup step. Give it search instead of a blank field to fill from memory. Breaks without it a tool that asks the model to type an ID instead of passing one along. ### 02 Look before it leaps Any action that writes or deletes gets a free dry run first: show what would happen before it happens, the way infrastructure tools print a plan before they apply it. Let the agent catch its own mistake while it's still cheap. Mine had them, and one still let a broken form through. It had a required file-upload field set up so that nobody could actually attach a file. The dry run passed clean. The form went out to a few hundred people, and in two weeks not one of them could complete it. A dry run only catches what it checks. Mine now also checks that every required field can actually be answered. Breaks without it a dry run that checks the easy parts and waves the rest through. ### 03 Say how sure you are, per tool Not everything in a toolset is equally trustworthy. Some tools I watched work live. Some I built from reading source code and never fired for real. I compared what I'd tested against what was just source-derived guesswork. Then I added guardrails specifically on the unverified stuff. Mostly that meant saying it in the tool descriptions: this part is live-verified, this part isn't, and an empty result here is not proof there's nothing. Example payloads came from real live requests, not hand-written guesses. Plain terms "verified" means I ran the tool against the real system and checked the result. "Unverified" means I wrote it from reading code and assumed it works. Both look identical to the agent unless you tell it otherwise. Before that, the guesswork showed. One tool, built on a response shape the server never actually returns, silently produced a fake "Unknown" row of zeros. Another failed about three calls in four while the logs showed zero errors. Breaks without it a guess that looks exactly like a verified answer. ### 04 Fix naming with a test, not a memory Similar names will eventually get confused for each other. Mine did, over one word. Asked to audit the work the team had done, the agent went looking for the system's audit trail instead, a feature with no tools behind it at all. Plain terms one set of tools reviews the audits a team did. The audit trail is the system's log of who changed what. Both are "audit", and the agent picked the wrong one. It still bites. In the benchmark in lesson 05, one setup on GPT sent audit questions away from the right tools every time and never found the one audit template. Zero out of five. Renaming fixes it once, until the next tool reopens the same ambiguity. The real fix is a regression test that keeps the fix in place. Plain terms a regression test runs on every change and fails if an old mistake comes back. Mine pins which words route where: "audit" goes to the team's audits, and every documented keyword goes where it should. Another came from a vague ID that cost about 13 wasted tool calls in production. Now the build fails if a tool name or input doesn't say which kind of thing it means. Breaks without it a word that means two things, and nothing stopping the next tool from bringing it back. ### 05 The real cost isn't the tool count There are three ways to hand an agent its tools: - The whole menu **All tools** Every tool, every time. - A guess **Scoped** My code guesses which part of the menu a question needs and loads only that. More gets unlocked mid-conversation if needed. - A search box **Deferred** A small core menu plus search. The agent looks up the rest by name and only pays for the full description of what it actually uses. Plain terms a tool's full description (its schema) is its name, what it does, and every input it takes. For a big toolset, those descriptions are most of what the model reads. **Assumed** More tools sitting in context means more tokens spent, every single turn. **Actual** Caching absorbs the repeats. The real hit is a cache miss: change the tool list mid-conversation and everything after the change is billed at full price again. In my logs, a single mid-conversation unlock rewrote 40–57K tokens of cache. Plain terms every turn, the model re-reads the whole conversation, including the tool menu. The vendor can save that reading (a cache write) so the next turn gets it at a fraction of the price. A cache miss throws the saving away, and you pay to write it all again. So watch how often the cache misses and how big each miss is, not how many tools you have. #### I tested it Two real conversations, a two-turn one and a harder three-turn one, five runs of each setup, on Claude Sonnet 5 and on GPT-5.6. Costs come from the raw token counts at list prices. Which setup wins depends on the vendor. Claude Sonnet 5 · two-turn All tools $0.16 All tools · $0.16 average · range $0.10–0.33 Scoped $0.33 Scoped · $0.33 average · range $0.24–0.58 Deferred $0.12 Deferred · $0.12 average · range $0.118–0.120 Claude Sonnet 5 · three-turn All tools $0.33 All tools · $0.33 average · range $0.23–0.37 Scoped $0.46 Scoped · $0.46 average · range $0.29–0.57 Deferred $0.32 Deferred · $0.32 average · range $0.22–0.40 **Show as a table** Claude Sonnet 5, average cost per conversation over five runs | Setup | Two-turn | Three-turn | | --- | --- | --- | | All tools | $0.16 $0.10–0.33 | $0.33 $0.23–0.37 | | Scoped | $0.33 $0.24–0.58 | $0.46 $0.29–0.57 | | Deferred | $0.12 $0.118–0.120 | $0.32 $0.22–0.40 | On Claude, deferred is the cheapest and the steadiest. Every two-turn run cost between $0.118 and $0.120. Scoped is the most expensive, because every unlock rewrites the cache. GPT-5.6 · two-turn All tools $0.09 All tools · $0.09 average · range $0.07–0.14 Scoped $0.16 Scoped · $0.16 average · range $0.11–0.20 Deferred $0.16 Deferred · $0.16 average · range $0.11–0.21 GPT-5.6 · three-turn All tools $0.13 All tools · $0.13 average · range $0.13–0.14 Scoped $0.26 Scoped · $0.26 average · range $0.19–0.37 Deferred $0.25 Deferred · $0.25 average · range $0.16–0.37 **Show as a table** GPT-5.6, average cost per conversation over five runs | Setup | Two-turn | Three-turn | | --- | --- | --- | | All tools | $0.09 $0.07–0.14 | $0.13 $0.13–0.14 | | Scoped | $0.16 $0.11–0.20 | $0.26 $0.19–0.37 | | Deferred | $0.16 $0.11–0.21 | $0.25 $0.16–0.37 | On GPT, the whole menu is cheapest, and deferred costs almost twice as much on the long conversation. Lesson 06 is why. What keeps deferred cheap on Claude is where new tools land. The tool list never changes: the agent's searches bring tools in as references inside the conversation, after the part that's already cached. Change the tool list mid-conversation, even just which tools are deferred, and the whole cached conversation is written again. That's what my 40–57K unlock did. Claude cost 1.3x more than GPT's cheapest setup on the short conversation and 2.4x more on the long one, mostly because it writes about three times as much. It also got more of the questions right on this interface. I haven't traced every miss, so I'm not calling that a model ranking. Five runs per setup, one test environment, list prices as of September 2026. An earlier one-run test got the shape right and the dollars wrong. *Corrected 30 September 2026: an earlier version said adding tools at the end of the list keeps the cache valid. The tool list comes first in the prompt, so that only holds when new tools arrive inside the conversation, as tool search's do. I built a tool to check for this, and wrote up what it found: [My MCP server changed its menu at the wrong time. Now it doesn't.](https://niksa.me/toolmenu)* ### 06 The same feature name can hide a different contract "Deferred tool loading" sounds like one thing. It's two. With Claude, the search runs inside the vendor's API: the model finds the tool itself, and my code never sees that step. With GPT, the search runs in my code, which then has to hand back what it found. - The vendor looks **Claude** Search runs inside the API. My code never sees the step. - I look **GPT** Search runs in my code, which hands the results back. Plain terms same name, different wiring. In one, the vendor does the looking. In the other, you do, and you pass the answer back. My code asked one yes-or-no question, "does this vendor support deferred loading?", and got yes for both. Nothing crashed. It just made the difference invisible, and the bill is where it showed. On GPT, every time my code hands the search results back, the cache gets rewritten: about 65K tokens of cache writes per conversation, against 15K with the whole menu loaded. It's also where the one crash in the benchmark happened. Same feature name, opposite result. Breaks without it one yes-or-no check standing in for two different behaviours. ### 07 A log line that means two things is worse than none I had a counter for cache misses, the exact thing lesson 05 says to watch. It fired every time the agent looked something up, even when nothing new had to load. Two of three test conversations reported misses that never happened. The AI I was working with read those lines and concluded a whole part of the system was being skipped. It wasn't. The counter just couldn't tell a real miss from a lookup. `cache.miss` tool list grew *real* `cache.miss` just a lookup *nothing loaded* Illustration, not the real log format. A line that reads the same in both cases is worse than no line. It invites a specific wrong inference, and it leans the same wrong way every time. **The fix:** count a miss only when something new actually has to load. This one didn't trip the agent using my tools. It tripped the AI helping me build them. Anything an AI reads is an interface: tool menus, logs, code comments. > The interface is yours to fix. It's not the model's job to route around your mistakes. ## What ties it together None of this came from theory. It came from watching the thing run, and treating every place it got confused as a design bug instead of a model limitation. The 115 tools were almost incidental. The agent reads everything around them too: schemas, names, validation, documentation, caching, telemetry. That's one interface, and all of it is yours to design. Justify a decision against a specific failure you observed, not one you imagined. Anticipated failures make for confident-sounding designs. Observed ones are the only kind that change what gets built. Niksa · September 2026