
This lesson is part of Learn MCP, our free course on the Model Context Protocol, within The MCP Guide.
In this lesson you will redesign the bookshop tools so a model picks the right one, calls it with valid arguments, and understands the result. You will apply the spec's naming rules, write descriptions for a model, tighten input schemas, add structured output, and set annotations honestly.
Getting a server to run is the easy part. A model only sees your tool names, descriptions and schemas. If those are vague, it calls the wrong tool, invents arguments, or skips your server entirely.
The code adapts the Python SDK's Tools guide and ran on 28 October 2026 with mcp 2.3.0. Rules are quoted from the 2026-07-28 tools specification.
Key Takeaways
- Design tools around user tasks, not around your API's endpoints. A few clear tools beat many thin ones.
- Tool names should be 1 to 128 characters of letters, digits,
_,-and., unique within the server. - Write each description for a model: what the tool does, when to use it, what it returns, and its limits.
- Use enums, bounds, patterns and defaults so bad arguments fail before your code runs, with a message the model can act on.
- Annotations are hints. If you omit them, the spec's defaults describe a tool that may be destructive, so set them deliberately.
Step 1: Start from tasks, not endpoints
Write down what a user would ask the AI to do with your server. For the bookshop:
- "Do you have anything by William Gibson?"
- "Order two copies of Dune."
- "Cancel my last order."
Now map tasks to tools. A good map has one tool per task, not one per database table. search_catalog, order_book and cancel_order cover those three requests. A tool for each REST endpoint (get_book_by_id, get_book_by_isbn, list_books_page) makes the model plan more steps and guess more often.
Our build guide's advice still holds: ship three good tools before you ship thirty. See how to build an MCP server.
Step 2: Name tools by the spec's rules
The tools spec says tool names should:
- be between 1 and 128 characters;
- be treated as case-sensitive;
- use only ASCII letters, digits, underscore, hyphen and dot;
- contain no spaces, commas or other special characters;
- be unique within a server.
Beyond the rules, name by action and object: search_catalog, cancel_order. Avoid query, run or do_action. The spec also warns that two servers may both expose a tool named search, and that clients should disambiguate, for example by prefixing a server identifier. A specific name lowers that risk. Our spec reference on tool names and descriptions has more examples.
Step 3: Write descriptions for the model
The description is the model's instruction manual. Include four things:
- What it does, in one sentence.
- When to use it, especially relative to your other tools.
- What it returns.
- Limits, such as scope, maximum results or side effects.
Compare:
| Weak | Better |
|---|---|
| "Search books." | "Find books in this shop's catalog. Use this before ordering to get an exact title. Returns up to limit matches with stock counts. Does not search other stores." |
| "Cancel." | "Cancel one unshipped order by its ID. Cancelling twice has no extra effect." |
Describe every argument too. In Python that is Field(description=...); in TypeScript it is .describe(). The TypeScript Tools guide notes that argument descriptions are "the only documentation the model gets for that argument."
Step 4: Make the schema do the validation
Tight inputs stop bad calls before they reach your code. Here are the redesigned tools:
from typing import Annotated, Literal
from pydantic import BaseModel, Field
from mcp.server import MCPServer
from mcp.types import ToolAnnotations
mcp = MCPServer("Bookshop")
class BookHit(BaseModel):
title: str
author: str
in_stock: int
@mcp.tool(
title="Search the catalog",
annotations=ToolAnnotations(read_only_hint=True, open_world_hint=False),
)
def search_catalog(
query: Annotated[str, Field(description="Words from the title or author name, e.g. 'gibson'.")],
genre: Literal["fiction", "non-fiction", "poetry"] | None = None,
limit: Annotated[int, Field(ge=1, le=20)] = 5,
) -> list[BookHit]:
"""Find books in this shop's catalog. Use this before ordering to get an exact title.
Returns up to `limit` matches with stock counts. Does not search other stores."""
return [BookHit(title="Neuromancer", author="William Gibson", in_stock=0)][:limit]
@mcp.tool(
title="Cancel an order",
annotations=ToolAnnotations(
read_only_hint=False, destructive_hint=True, idempotent_hint=True, open_world_hint=False
),
)
def cancel_order(
order_id: Annotated[str, Field(pattern=r"^ORD-\d{6}$", description="Order ID like ORD-004211.")],
) -> str:
"""Cancel one unshipped order by its ID. Cancelling twice has no extra effect."""
return f"Cancelled {order_id}."
What the schema now enforces:
genreis an enum. The model can only pick one of three values.limithas bounds and a default, so the model can leave it out.order_idhas a pattern. Callingcancel_orderwith"12"came back as a tool error saying the string "should match pattern '^ORD-\d{6}$'", and the function never ran.
The Python Tools guide calls this "self-correcting agents for free": the model reads the validation message and retries with a valid value.
Step 5: Return structured output
Because search_catalog returns list[BookHit], the SDK also advertised an outputSchema and returned structuredContent alongside the text. The model reads the text; the client app can use the typed data.
The tools spec says that if you declare an output schema, servers must return structured results that conform to it. For compatibility, a tool returning structured content should also return the serialized JSON as text. Both SDKs handle the text copy for you. Our reference on structured tool output covers the details.
Keep results lean. Return the fields the model needs for the next step, cap list sizes, and say when results were truncated. A tool that dumps 500 records wastes the model's context and your users' money.
Step 6: Set annotations honestly
Annotations tell clients how a tool behaves. The spec schema defines four hints and their defaults:
| Hint | Meaning | Default if omitted |
|---|---|---|
readOnlyHint | The tool does not modify its environment | false |
destructiveHint | The tool may perform destructive updates (only meaningful when not read-only) | true |
idempotentHint | Repeating the call with the same arguments has no extra effect (only meaningful when not read-only) | false |
openWorldHint | The tool may interact with external entities | true |
Read the defaults carefully. A tool with no annotations is described as possibly destructive, not idempotent and open-world. A host may ask the user to confirm every call. Mark read-only tools as read-only so hosts can treat them accordingly.
Two warnings. First, annotations are hints, not security. The spec says clients must consider annotations untrusted unless they come from trusted servers. Second, never mark a tool read-only when it is not. That misleads the user's approval decisions.
Step 7: Handle state with explicit handles
The 2026-07-28 spec has no protocol-level session. The tools spec has non-normative guidance for servers that need state across calls. Return an explicit handle, like a basket_id, from a creation tool, and accept it on later calls.
Design rules from that section:
- For authenticated servers, a handle is a name, not a capability. Check the caller's access on every call.
- Make handles opaque, not guessable.
- State the retention policy in the creation tool's description, such as "baskets expire after 24 hours of inactivity."
- Return a clear tool error for an expired handle, so the model can create a new one.
Step 8: Test with a real model
Schemas and descriptions are a hypothesis until a model uses them. Connect your server to a host and try 10 to 20 real requests, including vague ones. Watch for:
- the wrong tool picked;
- invented argument values;
- repeated failed calls;
- the model ignoring a tool you expected it to use.
Fix descriptions first; they are the cheapest change. The Inspector's tools/list --strict option from lesson 19 also checks your schemas for constructs some clients misread.
Try it
- Write down five real user requests for a server you plan to build.
- Map them to no more than four tools.
- For each tool, write the name, a four-part description, and an input schema with at least one enum, bound or pattern.
- Set all four annotations explicitly for each tool, and justify each value in a code comment.
- Run
tools/list --strictthrough the Inspector and fix anything it reports.
Recap
- Shape tools around tasks, name them by action, and follow the spec's naming rules.
- Write descriptions for a model: what, when, returns and limits.
- Let the schema reject bad arguments with readable messages.
- Return structured output, keep it lean, and set annotations on purpose.
Next step
This lesson ends the Building module. The next module, Security and Operations, starts with threat-modeling your server. For the protocol side of today's topics, read MCP tool annotations. The full outline is on the Learn MCP course page.
Course navigation
Previous lesson: Lesson 22: Add OAuth Sign-In.
Next lesson: Lesson 24: Threat-Model Your Server.
Back to the Learn MCP course outline.
Frequently asked questions
How many tools should one server have?
As few as cover the real tasks well. There is no spec limit, but every tool's name, description and schema compete for the model's attention. If two tools are often confused, merge them or sharpen their descriptions.
Should tool names use snake_case or camelCase?
Either is valid. The spec's own examples include getUser and DATA_EXPORT_v2. Pick one style and use it across the server.
Can I change a tool's name later?
You can, but it breaks saved prompts, host permissions and anything that refers to the old name. Treat names like a public API and version carefully. Lesson 27 covers versioning.
Do annotations change what the SDK does?
No. The TypeScript Tools guide says annotations never change how the SDK runs the tool. Hosts read them to decide what to show users and when to ask for approval.
Should I put examples in descriptions?
Short ones, yes, especially for formats like IDs or dates. Keep descriptions brief overall; a long description costs context on every request that lists your tools.











