Designing MCP tools your agents can't easily misuse
The Model Context Protocol makes it easy to expose your product to AI agents. Making those tools safe and reliable is a design problem, not a protocol problem.
The Model Context Protocol (MCP) has become the common way to connect AI agents to real systems. A server describes the tools it offers, each with a name, a description, and a JSON Schema for its inputs, and any compatible client can discover and call them.
The protocol makes connecting easy. It doesn't make the tools good. An agent decides which tool to call based almost entirely on names, descriptions, and schemas, and it will happily call the wrong one with plausible-looking arguments. Most of the reliability and safety of an agent integration comes from how those tools are designed.
Expose tasks, not your database
The tempting first version is a thin wrapper around internals:
{
"name": "run_query",
"description": "Run a SQL query against the production database.",
"inputSchema": {
"type": "object",
"properties": { "sql": { "type": "string" } },
"required": ["sql"]
}
}This gives the agent maximum power and minimum guidance. It has to guess your schema, it can write queries that scan entire tables, and nothing stops a destructive statement except the database user's permissions.
A task-shaped tool is narrower and much easier to use correctly:
{
"name": "find_orders",
"description": "Find a customer's recent orders. Use this to answer questions about order status or delivery. Returns at most 20 orders, newest first.",
"inputSchema": {
"type": "object",
"properties": {
"customer_email": { "type": "string", "format": "email" },
"status": {
"type": "string",
"enum": ["pending", "paid", "shipped", "delivered", "refunded"]
},
"since": { "type": "string", "format": "date" }
},
"required": ["customer_email"]
}
}The name says what it's for. The description says when to use it and what comes back. Enums replace free text wherever the set of valid values is known. The result is bounded.
Write descriptions for the model, and test them
Tool descriptions are prompts. Vague ones like "Gets data" lead to vague tool selection. Good descriptions answer three questions: what the tool does, when to use it instead of the similar-looking tool next to it, and what it returns.
Then test them like prompts. A handful of realistic requests per tool ("Where's my last order?", "Cancel the order I placed yesterday") run against your agent setup will quickly show whether it picks the right tool with the right arguments. Those requests make a good eval set as the tool list grows.
Separate reading from writing
Read-only tools can be generous. Tools that change state deserve more care:
- Make writes idempotent. Accept a client-generated request ID so a retried call doesn't create a second refund.
- Offer a dry run. A
preview_refundtool that returns what would happen lets the agent, and the user, check before committing. - Require confirmation for destructive actions. Many clients can ask the user before running a tool. Mark the dangerous ones so that happens, and keep truly irreversible operations out of agent reach entirely.
A useful rule: if you wouldn't give the action to a new support hire on their first day without a second check, don't give it to an agent without one either.
Authenticate as the user, not as an admin
An MCP server that connects to your API with a shared service account gives every agent session the same, usually broad, permissions. Prefer per-user credentials scoped to what that user can already do in your product. The agent then can't see or change anything the user couldn't, and your existing authorization rules keep working.
Treat tool output as untrusted input
Tool results go straight into the model's context. If a tool returns content that users or third parties control, such as support tickets, emails, web pages, or file contents, that content can contain instructions aimed at the model. This is prompt injection, and the defense starts with tool design:
- Return structured data instead of raw blobs where possible.
- Clearly separate data from instructions in what you return.
- Don't let a single session chain "read untrusted content" straight into "take a sensitive action" without a confirmation step in between.
Return small, useful results
Large tool outputs crowd the model's context and degrade its reasoning. Return the fields the task needs, paginate long lists, and include short hints for the obvious next step, like the ID needed to fetch more detail. Error messages matter too: "customer_email must be a valid email address" lets the agent correct itself, while a stack trace doesn't.
Log everything
Every tool call should be logged with its arguments, the user and session it belonged to, the result size, latency, and errors. When an agent does something surprising, this log is how you find out why. It's also the raw material for improving descriptions and catching misuse early.
The short version
- Design tools around user tasks, with clear names, guiding descriptions, and strict schemas.
- Keep writes idempotent, previewable, and confirmed when they matter.
- Use per-user, least-privilege credentials.
- Assume tool output can contain hostile instructions.
- Keep results small and errors actionable, and log every call.
MCP standardized the plumbing. The quality of what flows through it is still up to the people designing the tools.