MCP in Production: What the Tutorials Don't Cover
The Model Context Protocol won the "how do agents talk to tools" argument fast enough that there's already an enormous tutorial ecosystem, and every tutorial covers the same thing: expose a function as a tool, wire it to a client, watch a model call it. That part genuinely is easy. An afternoon, maybe less.
Then a second team starts depending on your server, an agent calls it ten thousand times a week, and you discover the tutorial covered about 10% of the job. The rest is the unglamorous engineering that makes any API production-grade, plus a few twists that come from your caller being a language model rather than a developer who read your docs. (If you need the recap: MCP is a protocol that lets AI applications discover and call tools, read resources, and use prompts exposed by a server. The spec at modelcontextprotocol.io covers it well, and that's the last basics will come up here.)
Credential scoping: the server is not the user
The tutorial pattern is a server holding one API key in an environment variable. That key silently grants every connected agent the union of its permissions, which is how a support agent ends up technically able to touch the billing system. Nobody decided that. It just happened, one env var at a time.
The fix is boring: authenticate your callers, not just your backends, and let per-caller scope decide which tools are even listed on a given connection. Don't rely on the model politely declining to call a tool it can see. An unlisted tool can't be called, can't be hallucinated into, and can't be reached by prompt injection. Filtering the tool list is the cheapest security boundary you will ever deploy.
Schema evolution: your caller doesn't read the changelog
Change a REST API and consumers break loudly, at compile time if you're lucky. Change an MCP tool schema and something stranger happens: agents keep calling it, because the caller is a model improvising against whatever schema it's shown today. Rename a parameter and yesterday's prompts start producing almost-right calls that fail validation. Or worse, calls that pass validation and mean something subtly different now.
So treat tool schemas with API-contract discipline:
1. Additive changes only; new parameters are optional with defaults.
2. Breaking changes ship as a new tool (search_orders_v2), and the old
one stays, deprecated and monitored, until its call volume hits zero.
3. Descriptions are part of the contract. Models route on them, so a "harmless"
wording tweak can shift calling behavior as much as a schema change. Version them,
review them, and cover them with evals,
because functionally they're prompts.
Failure handling: errors are prompts now
When a tool call fails, the error message goes into the model's context, and the
model decides what to do next based on what it says. This makes error strings
load-bearing in a way backend engineers aren't used to. ERR_TIMEOUT_5003
gives the model nothing, so it guesses, and its favorite guess is retrying the
exact same call. Compare "date_from must be YYYY-MM-DD, got '3/24'",
which gets a corrected call on the next step, or "rate limited — do not retry
for 60 seconds", which can head off the retry storm that was about to start.
Write every error message as an instruction to the caller, because that's what it
is now.
Two more habits that pay for themselves. Set server-side timeouts and make them visible in the response; an agent that never hears back doesn't fail cleanly, it stalls an entire run. And make every mutating tool idempotent, with a client-supplied idempotency key, because the one thing you know for certain about your caller is that it will eventually send the same call twice.
Observability: log the conversation, not just the request
Standard API logging tells you a request happened. Debugging an agent requires knowing why: which session, which step of the run, what had already been tried. Log every tool call with a correlation ID tying it to the agent trace, the full arguments, truncated results, latency, and outcome. When someone asks "why did the agent delete that record," the answer should be one query, not an archaeology project. The same log doubles as your audit trail, and as the dataset that shows which tools get misused, which descriptions confuse the model, and which of your error messages trigger retry loops.
A reference shape
Production MCP deployments tend to converge on this:
agents ──▶ gateway ──▶ MCP servers ──▶ backends
- authn per caller - thin tool logic - real systems
- tool-list filtering - schema versioning
- rate limits - typed errors
- call logging - idempotency keys
The gateway is the piece no tutorial mentions and the one you'll be gladdest you built. One place for authentication, scoping, rate limits, and logging, so each individual server stays thin and boring.
The production-readiness checklist
Before another team depends on your server:
☐ Callers are individually authenticated; no shared god-key.
☐ Tool listings are filtered per caller's scope.
☐ Schema changes are additive, or ship as a new versioned tool.
☐ Tool descriptions are under version control and covered by evals.
☐ Every error message tells the model what to do next.
☐ Mutating tools accept idempotency keys.
☐ Timeouts exist, and are reported rather than silent.
☐ Every call is logged with a trace correlation ID.
☐ Someone is watching deprecated-tool call volume before deletion.
None of this is exotic. It's the ordinary discipline of running an API, applied to a caller that reads your error messages literally, routes on your descriptions, and never reads your changelog. Do the boring 90% and MCP becomes what it promised to be: a reliable seam between your agents and everything else.