---
title: "Anti-Patterns"
description: "Ten common tool design mistakes and short fixes"
url: "https://agentsurface.dev/docs/tool-design/anti-patterns"
lastVerified: 2026-09-24
lastModified: 2026-09-25T05:46:01.000Z
---



## Summary [#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, embeddings, 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 `destructiveHint` and `idempotentHint`
* **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 [#1-terse-descriptions]

**Mistake:**

```typescript
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:**

```typescript
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 [#2-30-tools-on-one-agent]

**Mistake:**

```typescript
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](/docs/tool-design/tool-curation)).

**Fix:**

```typescript
// 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](/docs/tool-design/tool-curation).

***

## 3. Opaque IDs Without Lookup Tools [#3-opaque-ids-without-lookup-tools]

**Mistake:**

```typescript
// Tool returns
{
  results: [
    { user_id: "usr_123", status: "active" },
    { user_id: "usr_456", status: "active" },
  ];
}

// Agent cannot resolve what 'usr_123' is: no lookup tool
```

The agent cannot do anything with the result because it cannot resolve the ID to semantic content.

**Fix:**

```typescript
// 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) [#4-nested-union-types-openai-strict-mode]

**Mistake:**

```typescript
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:**

```typescript
// 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 result
```

**Pattern:** Flatten schemas. Use separate tools for distinct operation modes.

***

## 5. Optional Fields That Assume Omission Works Everywhere [#5-optional-fields-that-assume-omission-works-everywhere]

**Mistake:**

```typescript
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:**

```typescript
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](/docs/tool-design/schemas#nullable-parameters) for the full rule.

***

## 6. Inconsistent Naming Across Tools [#6-inconsistent-naming-across-tools]

**Mistake:**

```typescript
server.tool('create_user', ...);
server.tool('list_customers', ...);  // Why "customer" vs "user"?
server.tool('get_person', ...);
```

The agent is confused by inconsistent terminology.

**Fix:**

```typescript
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 [#7-side-effectful-tools-that-throw-errors]

**Mistake:**

```typescript
async (params) => {
  const user = await db.users.findById(params.id);
  if (!user) {
    throw new Error(`User not found`); // Unhandled exception
  }
  // ...
};
```

Uncaught exceptions produce MCP protocol errors with generic messages. Agents cannot recover.

**Fix:**

```typescript
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 [#8-destructive-tools-without-annotations]

**Mistake:**

```typescript
server.tool(
  "delete_invoice",
  "Deletes an invoice.",
  schema,
  handler,
  // No annotations
);
```

Downstream systems cannot tell this tool is dangerous.

**Fix:**

```typescript
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 [#9-no-disambiguation-between-similar-tools]

**Mistake:**

```typescript
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:**

```typescript
// 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 [#10-required-fields-with-no-examples]

**Mistake:**

```typescript
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:**

```typescript
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 [#bonus-marketing-copy-instead-of-prompting]

**Mistake:**

```typescript
"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:**

```typescript
"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 [#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 `destructiveHint` or called out in description
* [ ] All errors returned as `isError: true` responses, 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 idempotency keys) marked `idempotentHint: true`
* [ ] Optional fields targeting OpenAI strict mode are nullable and stay in `required`, not omitted

## See also [#see-also]

* `/docs/tool-design/naming-and-descriptions`
* `/docs/tool-design/schemas`
* `/docs/tool-design/idempotency-and-safety`
* `/docs/error-handling`
