Tool Design
Anti-Patterns
Ten common tool design mistakes and short fixes
Last verified 2026-09-24
Summary
Common tool design mistakes that cause routing errors, hallucinated parameters, and failed operations. Includes: terse descriptions (no "when to use"), >20 tools at once, opaque IDs (no semantic meaning), nested unions (OpenAI strict mode rejects), optional fields assumed to work everywhere (ambiguous under strict mode), inconsistent naming across tools, thrown errors (should return with isError: true), destructive tools without annotations, tools without discrimination ("get_data" too vague), and missing examples. Each includes a short fix pattern.
- 1. Terse descriptions: Add "Use when...", "Do not use for..."
- 2. >20 tools: Use dynamic selection (tool search, , or keyword filtering)
- 3. Opaque IDs: Use semantic values or detailed examples
- 4. Nested unions: Break into separate tools (OpenAI strict mode constraint)
- 5. Optional fields assumed to work everywhere: OpenAI strict mode needs nullable +
required, not omission - 6. Inconsistent naming: Pick one noun and use it across every tool
- 7. Throw errors: Return
{ isError: true, content: [...] } - 8. Destructive without annotations: Mark
destructiveHintandidempotentHint - 9. No disambiguation: One primary tool per intent; name variants explicitly
- 10. No examples: Provide input examples in schema
Poorly designed tools produce routing errors, hallucinated parameters, and failed operations. This page catalogs ten patterns that reliably cause failure and how to fix them.
1. Terse Descriptions
Mistake:
server.tool(
"search_issues",
"Searches for issues.", // Too vague
schema,
handler,
);The agent has no signal for when to use this tool vs similar ones, what the edge cases are, or how to construct inputs.
Fix:
server.tool(
"search_issues",
"Search for existing issues by text, assignee, or status. Use when finding tickets matching criteria. " +
"Do not use for creating new issues (use create_issue). Returns paginated results; use next_offset for subsequent pages.",
schema,
handler,
);Pattern: "What it does. When to use it. When NOT to use it. Edge cases."
2. 30+ Tools on One Agent
Mistake:
const agent = new Agent({
tools: [
search_issues,
create_issue,
update_issue,
// ... 27 more tools
],
});Agents cannot reason over large tool sets. OpenAI calls fewer than 20 functions at the start of a turn a soft suggestion; Anthropic reports selection accuracy degrading past roughly 30-50 available tools (see Tool Curation).
Fix:
// Role-based kits
const toolsForDeveloper = [
search_issues,
create_issue,
update_issue,
search_pull_requests,
create_pull_request,
// 5–8 tools total
];
const agent = new Agent({ tools: toolsForDeveloper });Pattern: Curate task-specific subsets. Use dynamic selection (a provider's tool search or embedding-based ranking) for large registries - see Tool Curation.
3. Opaque IDs Without Lookup Tools
Mistake:
// Tool returns
{
results: [
{ user_id: "usr_123", status: "active" },
{ user_id: "usr_456", status: "active" },
];
}
// Agent cannot resolve what 'usr_123' is: no lookup toolThe agent cannot do anything with the result because it cannot resolve the ID to semantic content.
Fix:
// Return semantic content alongside IDs
{
results: [
{ user_id: "usr_123", name: "Alice", email: "alice@example.com", status: "active" },
{ user_id: "usr_456", name: "Bob", email: "bob@example.com", status: "active" },
];
}
// Or provide a lookup tool
server.tool(
"get_user",
"Fetch user by ID. Required when search_users returns only IDs.",
schema,
handler,
);Pattern: Return semantic names, not opaque IDs alone. Or expose a paired getter.
4. Nested Union Types (OpenAI Strict Mode)
Mistake:
const schema = z.object({
result: z.union([
z.object({ type: z.literal("success"), data: z.any() }),
z.object({ type: z.literal("error"), message: z.string() }),
]),
});OpenAI strict mode rejects union types. Schema validation fails.
Fix:
// Flat discriminated union
const schema = z.object({
success: z.boolean(),
data: z.any().optional(),
error_message: z.string().optional(),
});
// Or use separate tools
server.tool('operation_dry_run', ...); // Returns preview
server.tool('operation_execute', ...); // Returns resultPattern: Flatten schemas. Use separate tools for distinct operation modes.
5. Optional Fields That Assume Omission Works Everywhere
Mistake:
const schema = z.object({
assignee: z.string().optional(), // Fine for most frameworks
});Most frameworks treat .optional() as "may be absent." OpenAI strict mode doesn't allow that - every property must be listed in required, so a genuinely absent field isn't expressible. A schema built only around plain .optional() silently fails strict-mode validation.
Fix:
const schema = z.object({
assignee: z.string().nullable(), // emits { "type": ["string", "null"] }, stays in `required`
});Pattern: For OpenAI strict-mode targets, keep the field in required and make it nullable instead of omitting it. See Schemas for the full rule.
6. Inconsistent Naming Across Tools
Mistake:
server.tool('create_user', ...);
server.tool('list_customers', ...); // Why "customer" vs "user"?
server.tool('get_person', ...);The agent is confused by inconsistent terminology.
Fix:
server.tool('create_user', ...);
server.tool('list_users', ...);
server.tool('get_user', ...);Pattern: Choose a noun and stick with it. user, not customer and person.
7. Side-Effectful Tools That Throw Errors
Mistake:
async (params) => {
const user = await db.users.findById(params.id);
if (!user) {
throw new Error(`User not found`); // Unhandled exception
}
// ...
};Uncaught exceptions produce protocol errors with generic messages. Agents cannot recover.
Fix:
async (params) => {
const user = await db.users.findById(params.id);
if (!user) {
return {
isError: true,
content: [
{
type: "text",
text: `User ${params.id} not found. Call list_users to find the correct ID.`,
},
],
};
}
// ...
};Pattern: Return errors in the response body so agents can reason about recovery.
8. Destructive Tools Without Annotations
Mistake:
server.tool(
"delete_invoice",
"Deletes an invoice.",
schema,
handler,
// No annotations
);Downstream systems cannot tell this tool is dangerous.
Fix:
server.tool(
"delete_invoice",
"Permanently deletes an invoice. This cannot be undone. " +
"Use only when the invoice was created in error.",
schema,
handler,
{
destructiveHint: true, // Signals danger
idempotentHint: false, // Not safe to retry
},
);Pattern: Mark all destructive tools with destructiveHint: true (MCP) or call it out in the description.
9. No Disambiguation Between Similar Tools
Mistake:
search_issues();
filter_issues();
query_issues();
find_issues();Four tools that do nearly the same thing. Agent has no signal for which to use.
Fix:
// One primary search tool with a clear description
server.tool(
"search_issues",
"Search for existing issues by text, assignee, or status. " +
"Use for finding tickets matching criteria. Returns paginated results.",
schema,
handler,
);
// If variants are genuinely needed, name them explicitly
server.tool("search_issues_simple", "Fast, limited filters for finding tickets.", schema, handler);
server.tool(
"search_issues_advanced",
"Full filter surface: status, assignee, labels.",
schema,
handler,
);Pattern: Have one primary tool per intent. Variants (simple/advanced) are acceptable if each is unambiguous.
10. Required Fields with No Examples
Mistake:
const schema = z.object({
date_range: z.string().describe("The date range to filter by"),
});What format is "date_range"? "2025-01-01 to 2025-01-31"? An ISO interval? A Slack-style "last_7_days"?
Fix:
const schema = z.object({
date_range: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}T\d{4}-\d{2}-\d{2}$/)
.describe(
'Date range in ISO format. Examples: "2025-01-01T2025-01-31", "2025-02-01T2025-02-28".',
),
});Pattern: Every parameter must have examples. Use regex to encode format constraints.
Bonus: Marketing Copy Instead of Prompting
Mistake:
"Harness the power of our best-in-class document management system " +
"to unlock the potential of your knowledge management workflows.";Not a prompt. The agent learns nothing about when to call this tool.
Fix:
"Search internal documents by keyword or metadata. Use for finding " +
"runbooks, architecture guides, and troubleshooting steps. " +
"Do not use for general knowledge (use web search). Returns snippets ranked by relevance.";Pattern: Write descriptions as instructions, not marketing copy.
Checklist
Before shipping a tool:
- Description includes "Use when..." and "Do not use for..."
- All parameters have
.describe()with examples -
`<20`tools per agent session (or use dynamic selection) - All responses include semantic content (names, not opaque IDs)
- Destructive tools marked with
destructiveHintor called out in description - All errors returned as
isError: trueresponses, never thrown - Naming consistent across tool set (same noun, verb_noun pattern)
- Read tools marked
readOnlyHint: true; write tools marked clearly - Idempotent tools (or those supporting ) marked
idempotentHint: true - Optional fields targeting OpenAI strict mode are nullable and stay in
required, not omitted
See also
/docs/tool-design/naming-and-descriptions/docs/tool-design/schemas/docs/tool-design/idempotency-and-safety/docs/error-handling