Study guides / CCDV-F / Domain 5

Tools & MCPs · Lesson 1 of 3

5.1 - Designing Tool Schemas Claude Can Use Reliably

Write tool descriptions and parameter schemas tight enough that the model picks the right tool and fills it in correctly, without guessing.

A tool's description is doing real work, not documentation for a human reader — it's the primary signal Claude uses to decide whether and when to call that tool over another, before it ever looks at the parameters. A vague description ("handles user requests") gives the model nothing to discriminate on; a specific one ("looks up a customer's order status by order ID; do not use for placing new orders") gives it a clear boundary, including an explicit negative case. The exam treats the description field as the single highest-leverage lever for fixing wrong-tool-selection bugs — before touching temperature, model choice, or prompt engineering elsewhere in the system.

JSON Schema mechanics of input_schema

Every tool's input_schema is a JSON Schema object: type: "object" at the top level, a properties map describing each field, and a required array naming which of those fields must be present. Each parameter benefits from the same specificity as the top-level description: a plain string named date invites format ambiguity; a description stating the expected format ("ISO 8601, e.g. 2026-08-28") removes it. Use enum whenever the valid values are a fixed, small set rather than free text — it doesn't just document the constraint, it narrows what the model can generate for that field in the first place. Nested object and array types are supported, but every extra level of nesting is another place a partially-specified generation can go wrong; prefer a flatter schema with clearly named fields over a deeply nested one that mirrors an internal data model.

Strict tool use guarantees the schema

Setting strict: true on a tool definition (a top-level field alongside name, description, and input_schema — not a tool_choice setting) guarantees that tool_use.input validates exactly against the schema. It requires additionalProperties: false and an accurate required array. Without strict, the schema is guidance the model usually follows; with it, malformed or missing-field tool calls become structurally impossible rather than merely unlikely.

Writing descriptions that change tool-selection behavior

Being prescriptive about when to call a tool, not just what it does, produces a measurable difference in whether the model reaches for it. "Get current weather for a location" describes the tool; "call this when the user asks about current conditions or the forecast for a specific place" gives Claude a trigger condition to match against the conversation. This matters more, not less, on newer Opus-tier models, which reach for tools more conservatively by default — a tool description without an explicit trigger condition is more likely to be skipped in favor of answering from the model's own knowledge, even when the tool would have produced a better answer. The same logic applies to negative cases: naming what a tool is not for ("do not use for placing new orders") is often more effective at preventing a specific miscall than any amount of positive-case detail on the correct tool.

Tool count, schema depth, and cognitive load

Every tool definition Claude sees consumes context and adds a candidate to choose between on every turn. A handful of tightly-scoped, clearly named tools (search_orders, create_order, cancel_order) outperforms one do-everything tool with a mode parameter (order_action with an action enum of search/create/cancel) for exactly the reason overlapping descriptions cause problems: the model has to do extra inference work to figure out which behavior you actually want, and that inference is where errors creep in. When a library of tools grows large (dozens to hundreds), the fix isn't cramming them all into every request — it's the tool search tool, which lets Claude discover relevant tools on demand instead of holding every schema in context simultaneously.

Common exam distractor

Two tools with overlapping, vaguely-worded descriptions is a common exam scenario for "why does the model keep calling the wrong tool" — the fix is tightening the descriptions to be mutually exclusive, not switching models, lowering temperature, or adding few-shot examples. A related distractor: blaming inconsistent tool selection on nondeterminism and reaching for a fixed seed or lower temperature. Sampling variance can make an ambiguous case flip between two plausible answers, but the underlying cause is still that the schema gave the model two equally-plausible options — fix the ambiguity, don't just narrow the sampling around it.

Testing and iterating on schemas

Because tool selection has some sampling variance, a single test call proving the "right" tool got picked doesn't prove the schema is fixed — and a single call proving the wrong tool got picked doesn't prove it's broken either. Run the same or similar prompts several times and look at the distribution, not one outcome. This matters most in exactly the boundary cases that motivate rewriting a description in the first place: a prompt that's genuinely ambiguous between two tools should ideally resolve to a clarifying question rather than a confident but wrong call, and testing across repetitions is how you tell the difference between "fixed" and "improved but still occasionally wrong."

Exam traps

Practice question

An agent has both a search_orders tool ("handles order-related queries") and a create_order tool ("handles order-related queries"), and it sometimes calls create_order when the user is just asking about order status. What's the most direct fix?

  • A Rewrite both descriptions to be specific and mutually exclusive about what each tool does and doesn't do. Correct

    The two descriptions are identical and give the model no way to discriminate - specific, non-overlapping descriptions directly fix the ambiguity causing wrong-tool selection.

  • B Remove create_order entirely so there's only one tool to choose from.

    This removes real functionality rather than fixing the actual problem, which is that the descriptions don't distinguish the two tools' purposes.

  • C Set tool_choice to any so the model is forced to call a tool.

    Forcing some tool call doesn't address which tool gets picked - the ambiguity between the two overlapping descriptions is the actual cause.

  • D Increase max_tokens so the model has more room to reason about which tool to pick.

    Output length isn't the constraint here - the tool descriptions themselves don't give the model enough signal to distinguish them.

Build exercise: Tighten two overlapping tool schemas

Beginner · 35 minutes

You'll practice:

  1. Define two tools with deliberately vague, overlapping descriptions (e.g., both saying "handles order-related queries"), send a prompt that should clearly favor one of them, and observe which tool gets called across 5 repeated calls. Then rewrite both descriptions to be specific, mutually exclusive, and prescriptive about when to call each, and re-test the same prompt 5 times.

    Seeing the wrong-choice behaviour firsthand, then fixing it, builds the diagnostic instinct faster than reading about it - and running multiple repetitions surfaces the sampling variance that a single test call would hide.

    You should see: Inconsistent or wrong tool selection across the 5 vague-description calls, and consistent correct selection across the 5 rewritten-description calls.

    Hints
    1. What does the Claude API give you to compare two tools without touching the model itself?
    2. Define both tools with input_schema, give them near-identical descriptions, and call client.messages.create with the same prompt 5 times in a loop, logging response.content for tool_use blocks and which name was picked each time.
    3. import anthropic
      client = anthropic.Anthropic()
      tools = [
          {"name": "search_orders", "description": "Handles order-related queries",
           "input_schema": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}},
          {"name": "create_order", "description": "Handles order-related queries",
           "input_schema": {"type": "object", "properties": {"items": {"type": "array", "items": {"type": "string"}}}, "required": ["items"]}},
      ]
      for _ in range(5):
          resp = client.messages.create(
              model="claude-opus-5", max_tokens=1024, tools=tools,
              messages=[{"role": "user", "content": "What's the status of order 48213?"}],
          )
          picked = next((b.name for b in resp.content if b.type == "tool_use"), None)
          print(picked)
  2. Add format guidance and an enum to at least one parameter on search_orders - e.g. a date_range string documented as ISO 8601, and a status filter constrained to an enum of ["pending", "shipped", "delivered", "cancelled"]. Send a prompt implying a status filter in natural language ("orders that haven't shipped yet") and confirm the model maps it onto a valid enum value rather than inventing free text.

    Enums don't just document a constraint - they narrow what the model can generate for that field, which is the schema-level equivalent of tightening a tool description.

    You should see: The tool_use input contains status: "pending" (a valid enum member), not a free-text phrase like "not yet shipped."

    Hints
    1. Where in a JSON Schema property definition do you restrict a string to a fixed set of values?
    2. Add "enum": [...] to the status property inside input_schema.properties, and describe each value's meaning in the property's description field so the model maps natural language onto the right member.
    3. "properties": {
        "order_id": {"type": "string"},
        "status": {
          "type": "string",
          "enum": ["pending", "shipped", "delivered", "cancelled"],
          "description": "Filter by order status; use 'pending' for orders not yet shipped."
        }
      }
  3. Set strict: true on the create_order tool definition, ensure its schema has additionalProperties: false and an accurate required array, then deliberately send a prompt that would previously have produced a malformed or partially-filled call. Confirm the call now either validates exactly or fails to be generated at all.

    strict mode moves the schema from guidance the model usually follows to a structural guarantee - the exam distinguishes 'the model usually gets this right' from 'this is enforced,' and this is where that distinction becomes concrete.

    You should see: tool_use.input for create_order always contains exactly the required fields with no extras, with no manual validation code needed on your side.

    Hints
    1. Which two things does a strict tool require in its input_schema besides required?
    2. Add "strict": true as a sibling of "name" and "input_schema" on the tool dict, and set "additionalProperties": false inside input_schema alongside a required array that lists every property Claude must supply.
    3. tools = [{
          "name": "create_order",
          "description": "Creates a new order for the given items. Call this only when the user explicitly wants to place a new order.",
          "strict": True,
          "input_schema": {
              "type": "object",
              "properties": {"items": {"type": "array", "items": {"type": "string"}}},
              "required": ["items"],
              "additionalProperties": False,
          },
      }]
  4. Rewrite both tool descriptions a second time to add explicit trigger conditions ("call this when the user is asking about the status of an existing order" / "call this when the user wants to place a new order") and explicit negative cases. Re-run the same 5-repetition test from Step 1 and compare the selection consistency.

    This isolates the effect of prescriptive when-to-call language from the effect of just being 'more specific' in general - the exam expects you to know that trigger conditions, not just detail, are what moves the selection rate.

    You should see: 100% consistent correct tool selection across the 5 repeated calls, an improvement over both the vague-description baseline and the merely-more-detailed version from Step 1.

    Hints
    1. What phrase structure turns a description from 'what this does' into 'when to use it'?
    2. Prefix or fold in a clause like 'Call this when...' and pair it with 'Do not use this for...' so the description encodes both the positive trigger and the exclusion in the same sentence.
    3. "description": "Looks up the status of an existing order by order ID. Call this when the user asks about an order they already placed (e.g. 'where is my order', 'has it shipped'). Do not use this to create new orders - use create_order for that."
  5. Simulate a large tool library by adding 15+ additional unrelated tool definitions to the tools list (stubs are fine), then measure whether tool-selection accuracy on the original two tools degrades as the list grows. If it does, note where the tool search tool (tool_search_tool_bm25_20251119 or tool_search_tool_regex_20251119) would apply instead of loading every schema into every request.

    The exam tests whether you know that tool count itself is a variable in selection accuracy, separate from description quality - and that the fix for a genuinely large library is dynamic discovery, not more prompt engineering on each description.

    You should see: A measurable drop in consistency (or at least noticeably more hedging or slower responses) once the tool list grows large, and a clear explanation of why tool search would be the appropriate next step rather than further description tuning.

    Hints
    1. What happens to the model's job as the number of candidate tools in a single request grows?
    2. Generate 15-20 stub tool dicts with generic names and minimal schemas, append them to the tools list alongside your two real tools, and rerun the Step 1 repetition test to see if selection consistency changes.
    3. stub_tools = [
          {"name": f"stub_tool_{i}", "description": f"Stub tool number {i} for testing",
           "input_schema": {"type": "object", "properties": {}}}
          for i in range(18)
      ]
      resp = client.messages.create(
          model="claude-opus-5", max_tokens=1024,
          tools=tools + stub_tools,
          messages=[{"role": "user", "content": "What's the status of order 48213?"}],
      )

Sources