Designing Tools for AI Agents
Good tool design is the difference between an agent that works and one that hallucinates. Learn the principles behind tools that LLMs read, understand, and call correctly every time.
Designing Tools for AI Agents
The most common reason agents fail in production is not the model. It is the tools. A model can only work with what you give it. If your tool names are ambiguous, your descriptions are thin, or your error messages say nothing useful, the agent will guess — and guessing is where things break.
This guide covers what separates a well-designed MCP tool from a bad one, and how to build tools that agents use correctly the first time.
The Core Principle: The Description Is the API
In traditional software, the function signature is the contract. For AI agents, the description is the contract.
The LLM never reads your source code. It reads the tool name and description you expose through the MCP schema. Everything the agent needs to know about when to use a tool, what to pass it, and what to expect back must live in that description.
This means descriptions are not optional documentation. They are load-bearing. Treat them with the same rigor you would treat a public API spec.
A description should answer three questions:
- What does this tool do?
- When should I use it (and when should I not)?
- What are the side effects or constraints?
// Bad: description tells the model almost nothing
{
name: "send_email",
description: "Sends an email.",
parameters: { ... }
}
// Good: description gives the model everything it needs to decide correctly
{
name: "send_email",
description: `Sends a transactional email to a single recipient via the configured SMTP provider.
Use this when you need to notify a user about an event, send a confirmation, or deliver a report.
Do NOT use this for bulk sends (use send_bulk_email instead) or for internal team alerts (use notify_slack).
This tool actually delivers the email — it is not a preview or a draft.
Rate limit: 100 calls per minute.`,
parameters: { ... }
}
The second version is longer. That is intentional. LLMs make better decisions when they have more signal. Do not optimize descriptions for brevity.
Naming Conventions That Work
Tool names are short identifiers the model uses to distinguish between dozens of available tools. Ambiguous names cause the model to pick the wrong tool or hesitate.
Follow these naming rules:
Use verb_noun format. The verb describes what the tool does, the noun describes what it acts on. get_user, create_lead, send_notification, analyze_sentiment. This makes tool lists scannable for the model.
Be specific over general. get_user_by_email is better than find_user. The specificity tells the model exactly when to reach for this tool versus a different lookup.
Avoid abbreviations. usr_mgmt_del is opaque. delete_user_account is clear. Models generalize better from full words.
Avoid overlapping names. If you have search_contacts and find_contacts, the model will struggle to distinguish them. Either merge them or make the difference explicit in the name: search_contacts_by_keyword versus find_contacts_by_tag.
// Bad naming examples
"do_thing"
"process"
"helper_v2"
"run_query_new"
// Good naming examples
"get_contact_by_id"
"create_campaign"
"delete_workflow"
"summarize_conversation"
Parameter Design
Parameters are where most tool bugs originate. The model has to construct parameter values from context — it cannot look them up. Design parameters to make that construction as unambiguous as possible.
Always Use Typed, Described Parameters
Every parameter needs a type and a description. The description should explain what value to pass, not just what the field is.
// Bad: no descriptions, model has to guess what format is expected
parameters: {
type: "object",
properties: {
date: { type: "string" },
range: { type: "string" },
format: { type: "string" }
}
}
// Good: descriptions specify format, constraints, and examples
parameters: {
type: "object",
properties: {
date: {
type: "string",
description: "Start date in ISO 8601 format (YYYY-MM-DD). Example: '2026-01-15'."
},
range: {
type: "string",
enum: ["7d", "30d", "90d", "1y"],
description: "Time range to query relative to the start date. Must be one of the allowed values."
},
format: {
type: "string",
enum: ["json", "csv", "markdown"],
description: "Output format for the results. Use 'json' for further processing, 'markdown' for display."
}
},
required: ["date", "range"]
}
Use Enums for Constrained Values
If a parameter only accepts specific values, use an enum. This prevents the model from hallucinating a value that looks plausible but does not exist in your system.
// Without enum: model might pass "high priority" or "urgent" or "1"
priority: { type: "string" }
// With enum: model is constrained to valid values
priority: {
type: "string",
enum: ["low", "medium", "high", "critical"],
description: "Task priority level. Use 'critical' only for incidents requiring immediate human review."
}
Mark Required vs Optional Explicitly
The required array in your JSON schema tells the model which parameters it must always supply. Be deliberate about this. If a parameter is optional, describe the default behavior when it is omitted.
parameters: {
type: "object",
properties: {
user_id: {
type: "string",
description: "The user's unique identifier. Required."
},
include_deleted: {
type: "boolean",
description: "Whether to include soft-deleted records. Defaults to false if omitted."
}
},
required: ["user_id"]
// include_deleted is optional — the model knows it can omit it
}
Input Validation Patterns
Validation is your last line of defense before a bad tool call does real damage. Validate at the tool boundary, not inside the downstream service.
Validate and Return Structured Errors
When validation fails, return an error the model can act on — not a generic failure. The model needs to know what was wrong and how to fix it.
async function create_contact(params: CreateContactParams) {
if (!params.email.includes("@")) {
return {
success: false,
error: "INVALID_EMAIL",
message: "The email address provided is not valid. Expected format: user@domain.com.",
field: "email",
received: params.email
};
}
if (params.phone && !/^\+\d{10,15}$/.test(params.phone)) {
return {
success: false,
error: "INVALID_PHONE_FORMAT",
message: "Phone number must be in E.164 format. Example: +14155552671.",
field: "phone",
received: params.phone
};
}
// proceed with creation
}
The agent can read this error, understand what went wrong, and retry with corrected parameters — without human intervention.
Validate Business Logic Too
Schema validation catches type errors. Business logic validation catches semantic errors.
async function schedule_campaign(params: ScheduleCampaignParams) {
const scheduledDate = new Date(params.send_at);
const now = new Date();
if (scheduledDate <= now) {
return {
success: false,
error: "SEND_DATE_IN_PAST",
message: `The send_at date must be in the future. Received: ${params.send_at}. Current time: ${now.toISOString()}.`
};
}
if (!params.recipient_list_id && !params.segment_id) {
return {
success: false,
error: "MISSING_RECIPIENT_TARGET",
message: "You must provide either a recipient_list_id or a segment_id. Both cannot be omitted."
};
}
}
Return Value Design
The model reads your tool's return value and uses it to decide what to do next. Design return values to make that decision easy.
Use Structured JSON, Not Plain Text
Plain text responses force the model to parse natural language to extract facts. Structured JSON gives it direct access to the data it needs.
// Bad: model has to parse this text to get the count
return "Found 3 matching contacts: John Smith, Jane Doe, and Bob Johnson.";
// Good: model can directly reference any field
return {
success: true,
count: 3,
contacts: [
{ id: "c_001", name: "John Smith", email: "john@example.com" },
{ id: "c_002", name: "Jane Doe", email: "jane@example.com" },
{ id: "c_003", name: "Bob Johnson", email: "bob@example.com" }
],
has_more: false
};
Always Include a Success Flag
Make it explicit whether the tool succeeded. Do not make the model infer success from the absence of an error field.
// Consistent return shape for success
{ success: true, data: { ... } }
// Consistent return shape for failure
{ success: false, error: "ERROR_CODE", message: "Human-readable explanation." }
Return Enough Context for the Next Step
Think about what the agent needs to do after calling this tool. Return data that enables the next action without requiring an extra roundtrip.
// After creating a user, the agent probably needs the ID for subsequent calls
return {
success: true,
user: {
id: "u_abc123", // agent can use this in follow-up calls
email: params.email,
created_at: new Date().toISOString(),
onboarding_url: `https://app.example.com/welcome/${token}` // agent can send this to the user
}
};
Good vs Bad Tool Design: Full Comparison
Here is a full before-and-after for a common tool pattern:
// BAD: generic, ambiguous, no guidance for the model
export const badTool = {
name: "update",
description: "Updates a record.",
parameters: {
type: "object",
properties: {
id: { type: "string" },
data: { type: "object" }
}
}
};
// GOOD: specific, descriptive, structured
export const goodTool = {
name: "update_contact_profile",
description: `Updates editable fields on an existing contact record.
Only include the fields you want to change — omitted fields are left unchanged.
Cannot be used to change email (use transfer_contact_email instead) or to delete a contact (use archive_contact).
Returns the full updated contact object on success.`,
parameters: {
type: "object",
properties: {
contact_id: {
type: "string",
description: "The unique ID of the contact to update. Format: 'c_' followed by alphanumeric characters."
},
first_name: {
type: "string",
description: "Updated first name. Omit if unchanged."
},
last_name: {
type: "string",
description: "Updated last name. Omit if unchanged."
},
phone: {
type: "string",
description: "Updated phone in E.164 format (+14155552671). Omit if unchanged."
},
tags: {
type: "array",
items: { type: "string" },
description: "Replaces the entire tag list. To add a tag without removing others, fetch current tags first."
}
},
required: ["contact_id"]
}
};
The 5 Rules of Tool Design
- The description is the API. Write it like a contract, not a comment.
- Use enums for constrained values. Never let the model guess from an open string.
- Return errors the model can act on. Include the error code, the reason, and the field that failed.
- Return structured JSON. Give the model data, not text to parse.
- Name tools like API endpoints. Verb-noun, specific, no abbreviations.
Follow these rules and your tools become the foundation of a reliable agent. Violate them and you are debugging hallucinated parameters at 2am.
Next Steps
With solid tool design in place, the next layer is making sure agents stay within safe boundaries when they use those tools. See Guardrails and Safety for how to add hard limits, confirmation gates, and output filters that keep your 15-agent system from doing things you did not intend.