MCP Servers Aren't the Hard Part: What Actually Breaks Production Agents

Every "MCP tutorial" on the internet ends the same way: you spin up a local server, connect it to Claude Desktop, call one tool, and it works. Cool. Ship it. Except that's the easy 10% of the job. Nobody writes the follow-up post about what happens six weeks later when the agent is calling your MCP server in production, tool selection starts drifting, and you're staring at logs trying to figure out why the model picked the wrong tool three times in a row.
I build agent systems for a living now, and MCP is the backbone of most of them. It's a genuinely good idea, standardized tool discovery and invocation instead of every team hand-rolling their own function-calling glue. But the protocol was never the hard part. The hard part is everything the protocol doesn't tell you to think about.
The Protocol Isn't the Bottleneck, Your Tool Design Is
MCP gives you a clean way to expose tools, resources, and prompts to a model. That's it. It doesn't tell you how many tools is too many, how to name them, or how much context each tool description should eat. Those decisions are entirely on you, and they matter more than anything about the transport layer.
The single biggest mistake I see, and made myself early on, is exposing every possible operation as its own tool because it feels "more precise." I once had an MCP server with 34 tools connected to one agent. The model started confusing update_record with patch_record because both existed, both sounded reasonable, and the descriptions were 80% identical. The fix wasn't better prompting. It was deleting 20 tools and merging overlapping ones into a smaller set with genuinely distinct purposes.
Context Rot Is Real, and It Has Nothing to Do With Token Limits
People assume context problems are about running out of room. In practice, you run out of attention long before you run out of tokens. Every tool schema you register sits in the model's context on every single turn, whether it gets used or not. Load in 40 tools with verbose descriptions and you've spent a meaningful chunk of the context budget before the user has said a word, and the model now has to weigh 40 options instead of 6 on every decision.
What actually helped:
- Group by workflow, not by API endpoint. If your backend has 12 CRUD endpoints for one resource, that doesn't mean you need 12 tools. Most agents only need 3 or 4 verbs that map to how a human would actually describe the task.
- Write tool descriptions like you're briefing a new hire, not documenting an API. "Searches customer records" is worse than "Use this to find a customer by name, email, or phone when the user hasn't given you an ID yet."
- Load tools dynamically when you can. If your orchestrator knows the task is billing-related, don't hand the agent your entire inventory-management toolset on that turn.
Silent Failures Are Worse Than Loud Ones
Tools fail. That's fine, expected even. What's not fine is a tool that fails silently and returns something that looks like a valid response. I had a tool that hit a rate limit and returned an empty array instead of an error. The agent interpreted that as "no results found" and confidently told a user something was out of stock when it just hadn't been able to check.
Every tool response your MCP server returns should make failure unambiguous. Return structured errors with a reason the model can reason about, not a 200 with an empty payload. Agents are only as good as the honesty of the tools feeding them, and an agent that trusts a lying tool will lie to your user with total confidence.
Idempotency Is Not Optional
Agents retry. Sometimes because of your own orchestration logic, sometimes because the model itself decides to "try again" after an ambiguous result. If your create_invoice tool isn't idempotent, you will eventually get duplicate invoices, and you will find out from an angry customer, not from your logs. Every state-changing tool needs a way to detect and reject a duplicate call, whether that's an idempotency key the model passes through or a dedupe check server-side.
What Actually Works
- Fewer, sharper tools beat many granular ones. If two tools are ever confused for each other, merge them or rename them until they aren't.
- Log every tool call, input and output, in production. You cannot debug agent behavior from vibes. When something goes wrong, you need the exact call the model made and exactly what it got back.
- Version your tool schemas. Changing a tool's shape without warning breaks every prompt and every eval you've built around the old version.
- Keep a human review step for anything that writes data or costs money. Read-only tools can move fast. Anything with side effects should have a checkpoint until you've watched it behave correctly for a while.
- Treat the orchestrator's tool-selection prompt as its own product. It deserves as much iteration as your system prompt does, because it's the thing deciding whether the right tool gets called at all.
Where This Leaves Us
MCP made the plumbing easier, and that's genuinely valuable. But it also means the plumbing is no longer the excuse. The teams shipping agents that actually hold up in production aren't the ones with the cleverest protocol implementation. They're the ones who treated tool design, error handling, and observability as first-class engineering problems instead of an afterthought bolted onto a demo.
If you're building on MCP right now, spend less time on the server boilerplate and more time asking whether a human could tell your tools apart by their descriptions alone. If you can't, neither can the model.