Model Context Protocol servers have gone from prototype to production fast. The wire format is simple, the SDKs are decent, and any agent that wants to ship custom tooling reaches for them. We audited 23 production deployments over the last quarter. Four issues showed up in nearly all of them.
This is the short fix list. Not a survey paper.
1. The "introspection by default" problem
Most MCP server SDKs ship with a tools/list endpoint that returns the full schema of every registered tool. Including ones the calling agent has no permission to invoke.
This is fine in development. In production it's a recon tool for an attacker who has a model in your environment. They ask the model what tools are available, the model calls tools/list, and now they have a complete inventory of your internal capabilities — including descriptions, parameter schemas, and any "internal use" tools you forgot to gate.
Fix: gate tools/list behind the same auth as the tool calls themselves, and return only the tools the caller is actually authorized to invoke. Two of three popular SDKs make this an opt-in. It should be the default.
2. Tool descriptions leak through to the model
Every tool you register has a description. The MCP spec encourages you to make these rich so the model can decide when to call them. That description goes into the model's context as instruction-level text.
We saw deployments where tool descriptions included:
- Internal API endpoint URLs (
"Calls our internal billing service at https://internal-billing.example/v2"). - Names of other services (
"Use this when the user wants to escalate to the customer-success team in Slack channel #cs-escalations"). - Implicit credentials by reference (
"Requires the on-call engineer's PagerDuty token").
The model is now repeating these in responses when an attacker prompts for them.
Fix: treat tool descriptions as user-visible documentation, not internal notes. If a sentence wouldn't be safe in your public API docs, it doesn't belong in the description.
3. No request budget per tool call
Most MCP server implementations run each tool call to completion. There's no timeout, no token budget, no recursion depth. We had several engagements where we caused the server to recurse on itself — tool_a called tool_b which called tool_a again — until the process died from memory exhaustion.
This isn't an MCP-specific bug, but the protocol's design makes it especially easy to trigger because tools commonly call other tools.
Fix: every tool handler gets a per-call timeout, a per-call wall-clock budget, and a recursion-depth limit checked against the call stack. The SDK should provide these as decorators. Most don't.
4. The "trusted prefix" assumption
A surprisingly common pattern in tool handlers:
def handle_search_web(query: str):
# Validate the query is safe
if not query.startswith("SAFE:"):
return "Invalid query"
return do_search(query[5:])The reasoning: "The model adds SAFE: to queries it considers safe; we use the presence of this prefix as a permission check."
The model is the same model an attacker can talk to. They tell it to add SAFE: and it does. The check is the security equivalent of a coat of paint.
We saw four variations of this in different shapes. In one, the prefix was a base64-encoded JSON object the model was supposed to construct. The attacker just learned the format from a few example error messages and constructed valid ones.
Fix: every authorization decision in a tool handler should be based on a property of the caller — their authenticated identity, their session token, their permission set — not a property of the input. If the model is generating the property you're checking, the property is meaningless.
The audit checklist
If you operate an MCP server, run through this:
- [ ]
tools/listreturns only authorized tools per caller - [ ] Tool descriptions don't leak internal infrastructure details
- [ ] Each tool handler has timeout + budget + recursion limits
- [ ] All authorization decisions are based on caller identity, not input contents
- [ ] Tool input validation rejects unknown fields rather than ignoring them
- [ ] Tool output validation strips fields the schema doesn't promise
- [ ] You have one log entry per tool invocation with caller, tool, args (redacted as needed), and result code
We hit at least three of these in every audit. The first four catch the majority of real-world incidents we've worked.
What we'd like to see in the spec
A small wishlist for the MCP working group:
- A standard authorization context object passed to every tool handler — caller identity, scopes, original request envelope — so handlers can't accidentally trust input fields they shouldn't.
- Required schema for tool descriptions that flags fields as "model-visible" vs "operator-only", with the SDK enforcing the split.
- A baseline rate-limit and budget object in the SDK so handlers don't have to build their own.
The protocol is in a good place. The defaults around it need to catch up.
Disclosure note: three of the issues we found rose to coordinated CVE disclosure. The other twenty were framework-pattern findings we shared with each team privately. The 23 deployments included three open-source frameworks, eleven commercial SaaS integrations, and nine in-house implementations.