Designing Tools Well Chapter 4 of the book "MCP in Practice" instructs software engineers building production-ready Model Context Protocol tools to treat the tool description as the model's user interface, since the tools/list response — a name, description, input schema and annotations — is all the model ever sees. The chapter applies a five-element checklist (data source, scope, time behavior, result limits, side effects) to a sample HTTP status lookup tool, contrasting a weak description with a revised one that specifies a built-in table of standard HTTP status codes, integer codes from 100 to 599, a one-line usage note per call, and read-only behavior. The chapter also advises naming tools so a model scanning twenty tools from six servers can match them against the user's words before reading descriptions. This is Chapter 4 of MCP in Practice, a book for software engineers building production-ready MCP tools. Previously: Chapter 1 — The Integration Problem, Chapter 2 — MCP Core Concepts, and Chapter 3 — Your First MCP Server. Disclosure: this article was drafted with AI assistance and reviewed by the author. Designing Tools Well The tool definition is the user interface the model actually reads What you will understand Why the tool description is the model’s user interface, and the five elements it must carry How to name tools so the model picks the right one without reading your code How to design input schemas the model can fill correctly on the first try When to split one tool into two, and when to merge two into one How to write errors, annotations, and versions that survive contact with real agents The description is the model’s user interface Your server’s code never reaches the model. What reaches it is the tools/list response: a name, a description, an input schema, and a few annotations. That is the entire user interface. Chapter 2 gave you a five-element checklist for judging descriptions — data source, scope, time behavior, result limits, side effects. This chapter puts that checklist to work, because each element answers a question the model or the host is actively asking at the moment of the call. Watch what happens with a weak description. Suppose the http-status server from Chapter 3 had shipped this instead of the description it actually carries: {"name": "lookup http status","description": "Looks up HTTP status codes."} The model learns nothing it needs: which codes are covered, what comes back, whether the call is safe to make without asking the user. A host deciding whether to auto-approve the call is equally blind — “looks up” could mean reading a built-in table or scraping a website. Now the revision the server actually ships: “description”: “Look up the meaning of an HTTP status code. Searches a built-in table of standard HTTP status codes; it makes no network calls. Returns the code’s category, standard phrase, and a one-line usage note. Read-only: it never modifies anything. Accepts integer codes from 100 to 599 and raises an error for anything outside that range.” Read it against the checklist, and notice what each element buys: Data source: “a built-in table of standard HTTP status codes.” The model will not assume it can verify against the live web, and it will not ask the user which table you meant. Scope: “integer codes from 100 to 599.” The model knows what to pass and — just as important — what will be rejected, so it can validate its own arguments before calling. Time behavior: a fixed reference table. The answer is deterministic and does not go stale, which tells the model there is no reason to call twice “just to check.” Result limits: “the code’s category, standard phrase, and a one-line usage note.” One record per call, in a known shape — the model can plan what to do with the result before it arrives. Side effects: “Read-only: it never modifies anything.” This sentence is not for the model at all. It is for the host, which uses it — along with the annotations below — to decide whether the call needs the user’s confirmation. Notice what the description does not do: it does not teach the model HTTP. The model already knows, roughly, what a 404 is. The description teaches the model about your tool — its boundaries, its cost, its shape. Write for the tool, not the domain. A description that explains the subject matter is documentation for a human; a description that explains the contract is an interface for the model. Name tools like the model is reading a menu When a host shows the model twenty tools from six servers, the model scans names first. Names are compared side by side, sometimes truncated in a user interface, and matched against the user’s words. A good name does half the description’s job before the description is even read. Lead with a specific verb. lookup http status beats get data; search production traces beats query. “Get,” “run,” “process,” and “handle” describe nothing — they are the verbs you reach for when you have not decided what the tool does. Name the domain, not the implementation. lookup http status, not query reference table. The model thinks in tasks, not in your storage layout, and the name should meet it where it thinks. Keep names comparable across a server. If one tool is list invoices, its sibling is get invoice, not fetchBill. Every inconsistency forces the model to memorize your thesaurus instead of your tools. Respect the spec’s naming guidance: names should stay within 1 to 128 characters, are case-sensitive, and should stick to letters, digits, underscore, hyphen, and dot — no spaces. Uniqueness within a server is not a nicety; it is how a tool gets addressed at all. Compare two catalogs for the same server: get data, fetch info, run query lookup http status, lookup status by category The first catalog forces the model to read every description just to tell the tools apart — and descriptions are longer than names, which means more tokens spent before the first decision is even made. The second catalog decides at the name. That is not a cosmetic difference; it is fewer chances to choose wrong, on every single call, for the life of the server. A useful test: cover the descriptions and read only the names. Can you still tell which tool does what — and, harder, which two are dangerously similar? If lookup http status and lookup http category blur together at a glance, the names are not doing their job. That blur is also a design signal, and the next section gives it a structural answer. Design the input schema as a contract The input schema is where the model’s creativity meets your server’s discipline. The model is a fluent writer of JSON and an unreliable follower of implications. Every constraint you leave unstated becomes a guess, and every guess becomes a failed call the model must recover from. The schema is the one place where you can remove guessing entirely — treat it as a contract, not a suggestion. Make required truly required and optional truly optional. FastMCP derives this from your function signature: parameters with defaults become optional. If the tool cannot do its job without an argument, give it no default and say so. Prefer enums over free text wherever the valid set is closed. A category parameter typed as a bare string invites “client-errors,” “4xx,” and “client error” — three spellings of one idea. An enum of five fixed values removes the negotiation before it starts: "category": { "type": "string", "enum": "informational", "success", "redirection", "client error", "server error" , "description": "Which 1xx–5xx class to list. Use the singular lowercase value exactly."} Describe every parameter. The per-parameter description is the model’s inline help at the exact moment it fills the argument: the units, the format, and one example. A parameter without a description is a question you declined to answer. Never build a mega-parameter: one query: str that accepts “anything the user might mean.” If the tool needs a service, a time window, and a filter, those are three parameters with three types — not one string your server parses with hope. Bound what the domain bounds. maxLength on free text, minimum and maximum on numbers. The schema rejects cheaply what your code would otherwise reject expensively, and the rejection happens before your function ever runs. Test the schema the way the model will use it: hand the tools/list output to someone who has never seen your server and ask them to write three calls — one normal, one edge case, one deliberately wrong. Every hesitation is a description you still owe, and every wrong guess is a parameter that needs a tighter type. One job per tool: when to split and when to merge Chapter 3’s first mistake was the everything server. The subtler version is the everything tool: one tool that grows a second job because “it’s related.” Related is not the test. The test is whether the two jobs share the same contract with the caller — the same side effects, the same trust, the same approval story. When they don’t, one tool is really two tools wearing a trench coat. Split when any of these differ: Side effects. A tool that reads and a tool that writes must never be one tool with a mode flag. The host approves tools, not modes — it cannot auto-approve half a tool, and the model cannot tell which half it just invoked. Data source or authority. lookup http status reads a built-in table. A tool that fetched live definitions from the web would carry different freshness, latency, and trust. Different descriptions, different tools. Approval needs. If one job needs the user to confirm and the other does not, they are two tools. Approval attaches to the tool, never to a parameter value. Failure modes. If half the calls fail for reasons the other half never encounters, the error story has already split — the tool should follow. Merge when the job is genuinely one job and only the parameters vary: an optional verbose flag beats a second tool when the data source, side effects, and result shape are identical and only the detail level changes. The flowchart below is the whole decision: When in doubt, split. Two narrow tools with clear names are easier for the model to choose between than one broad tool whose description is full of “or.” Merging is cheap later; untangling a tool that callers already depend on is not — which is a versioning problem, and we will get there. Write errors the model can recover from Chapter 3 drew the line: invalid input raises and becomes an error result; a valid query with a negative answer returns a normal result. That line is the foundation. This section is the architecture built on it — because errors are the model’s only debugging channel, and a tool whose errors don’t teach is a tool the model will misuse twice. The spec gives tools two error channels, and the distinction matters for design. Protocol errors report problems with the request itself — an unknown tool, a malformed envelope — things the model is unlikely to fix. Tool execution errors report problems with the operation: a validation failure, a downstream outage, a business rule. Clients are expected to hand execution errors to the model so it can self-correct, which makes the error message the most leveraged text you will write after the description. A recoverable error has four parts: Name what was wrong, with the value. “code must be between 100 and 599, got 99” lets the model see its mistake and the fix in one line. “Invalid input” names nothing and fixes nothing. Say what to do next. Append the remedy to the message: “Pass an integer from 100 to 599.” An error without a next step is a dead end the model must reason its way around, usually by guessing again. Keep the shape machine-readable. A stable code the caller can switch on, plus the human message — correlated to the request by id, exactly as Chapter 3 showed. Humans read the message; programs read the code; both need to be there. Say whether retrying helps. A validation error is retryable with different arguments; an expired handle means create a new one first Chapter 2’s stateful-tools pattern ; a downstream outage means wait or tell the user. Name the category, or the model will retry the unretryable and abandon the fixable. The difference in practice: Bad: “Invalid input.” Good: “code must be between 100 and 599, got 99. Pass an integer from 100 to 599.” The first message ends the conversation — there is nothing in it to act on, so the model either retries the identical call or abandons the task. The second contains the value, the rule, and the remedy in one line. Write every error as if the reader is a capable colleague who simply cannot see your code, because that is exactly who it is. And one prohibition: never leak internals. No stack traces, no file paths, no SQL, no secret-adjacent detail. The model does not need your traceback to fix its arguments, and the user should never see it. Log the internals to stderr for yourself; send the caller only what the caller can act on. Annotate behavior honestly Descriptions tell the model what a tool does. Annotations tell the host what kind of thing it is. The spec defines an optional annotations object carrying four behavior hints — readOnlyHint, destructiveHint, idempotentHint, openWorldHint — plus a human-readable title. They are hints: advisory metadata a client may use for confirmation dialogs and display. They are not enforcement. Your server enforces its own safety; annotations merely advertise it, and the host decides what to believe. readOnlyHint: true means the tool modifies nothing. Set it on lookup http status — it is the difference between the host auto-approving the call and interrupting the user to ask. A read-only tool without the hint gets treated with suspicion it has not earned. destructiveHint: true marks irreversible change. Note the spec’s defaults are deliberately conservative: an unannotated tool is treated as potentially destructive, non-idempotent, and externally reaching. Silence is not neutral — it is the strictest possible reading of your tool. idempotentHint: true promises that repeating the call with the same arguments has no additional effect. Claim it only when it is true; hosts and models lean on it when deciding whether a retry is safe. openWorldHint: true means the tool reaches outside the server — network calls, external APIs. Our http-status tool reads a built-in table, so its honest value is false. The day the table becomes a live fetch, the annotation must change with the code. title gives humans a display name — “Look up HTTP status code” — so name can stay a programmatic identifier. Names are for addressing; titles are for reading. As it appears in tools/list, the whole declaration is small and worth getting exactly right: "annotations": { "title": "Look up HTTP status code", "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": false} The honesty rule is absolute: annotate what the tool does, not what you wish it did. A read-only tool that secretly writes is not a documentation bug — it is a trust violation the host cannot detect, because the host’s approval policy reads your annotations, not your intentions. Review annotations with the same seriousness as the description, and re-review them every time the tool’s behavior changes. Design for retries: idempotency and side effects Networks fail, hosts time out, and models retry. Any tool with a side effect will eventually be called twice for one intention — once by the user, once by the retry. The second call is not a bug in the caller; it is a fact about distributed systems. Design for it before it happens, because retrofitting safety onto a tool that already mutates is where outages are born. Make side-effecting tools idempotent where the domain allows. Accept a client-supplied idempotency key and key the operation on it, or define the operation so repetition converges: setting a value, not incrementing one; ensuring a state, not appending to it. Read before you write. A tool that checks current state before changing it turns “create” into “ensure,” and “ensure” survives retries that would duplicate a blind “create.” Offer a dry run where the stakes are high. A preview parameter that returns what would happen without doing it gives the model a safe way to confirm its plan — and gives the user something concrete to approve. Keep approval in the host. Chapter 2 placed policy there for a reason: the server validates inputs and enforces its own authorization, and the host decides whether the user must confirm. Do not invent a private confirmation dance inside the tool — the host cannot see it, audit it, or apply policy to it, which makes it theater, not safety. Version tools without breaking callers The first time you rename a tool, you learn who your callers are: every host configuration, every saved prompt, every agent workflow that named the old string breaks at once. Tools are a public API with an unusual property — one of the clients is a language model whose knowledge of your names came from context it may never see again. Version accordingly. Add; don’t change. New optional parameters, new tools, new enum values. Never rename a tool, remove a parameter, or narrow an accepted type. Additive changes are invisible to existing callers; anything else is a migration you are imposing on strangers. A new required parameter is a breaking change, full stop. If the job genuinely needs new input, ship a new tool and deprecate the old one in its description: “Prefer lookup http status v2; this tool remains for compatibility.” Two tools with an honest pointer beat one tool with a surprise. Deprecation lives in the description, because that is where the model actually reads. Say what replaces the tool and, if you know it, when the old one stops working. A deprecated tool that still works is a courtesy; a removed tool is an outage wearing a version number. A deprecation done well reads like this, in the old tool’s description: “Deprecated: use lookup http status v2, which adds an optional detail parameter. This tool remains available and unchanged.” The model sees the pointer at decision time and simply never chooses the old tool again. No migration guide, no breakage — the catalog heals itself one call at a time. Treat tools/list as the live contract. If your catalog changes, the listChanged capability and subscription notifications exist so hosts can refresh — Chapter 2’s advice to design for catalogs that change applies to your own server first. Seven tool-design traps These are not hypothetical. They are the failures that show up in real tool catalogs, including the author’s, usually discovered by watching a model misuse a tool that tested perfectly by hand. The helpful description. “Searches things and returns stuff.” Vague descriptions don’t fail loudly — they fail as wrong tool choices you will never see, because the model quietly picks the wrong tool and moves on. The fix is the five-element checklist, applied to every tool, not just the important ones. The mode flag. One tool, one boolean, two jobs: read versus write, preview versus execute. The host approves tools, not modes, so half your tool inherits the other’s trust level. The fix is a split, however inelegant two tools feel. The mind-reading schema. query: str, options: dict — parameters the model must guess at. Every guess is a failed call followed by a recovery, and recoveries cost the user’s patience before they cost tokens. The fix is narrow types, enums, and a description on every parameter. The silent side effect. A “lookup” tool that also logs, caches, or notifies — undeclared, unannotated. The host auto-approved a read and got a write. The fix is honesty in both places: the description names everything the tool does, and the annotations match. The dead-end error. “Invalid input.” No value, no rule, no next step. The model retries the identical call or abandons the task. The fix names the value, the rule, and the remedy — and says whether retrying helps. The clever rename. lookup http status becomes getHttpStatus for “consistency,” and every saved workflow, prompt, and host config breaks at once. The fix is additive change only, with deprecation declared in the description where the model will actually read it. The untested choice. You tested the tool; you never tested whether the model picks it. Descriptions are UI, and UI gets usability-tested. The fix is adversarial selection testing in the Inspector: give the model a task and two similar tools, and watch which one it reaches for before you ship the catalog. Chapter checkpoint The tools/list response — name, description, input schema, annotations — is the entire user interface the model sees. Design it like one. A strong description carries five elements: data source, scope, time behavior, result limits, and side effects. Each answers a question the model or host is asking right now. Names are scanned side by side: lead with a specific verb, name the domain, keep siblings consistent, and follow the spec’s naming guidance. Input schemas are contracts. Required means required, enums beat free text, every parameter gets a description, and mega-parameters are a design smell. Split tools when side effects, data sources, approval needs, or failure modes differ; merge when only parameters vary. When in doubt, split. Errors are the model’s debugging channel: name the value, the rule, and the remedy; keep the shape machine-readable and correlated by id; never leak internals. Annotations are honest, advisory metadata — readOnlyHint, destructiveHint, idempotentHint, openWorldHint, plus title. Defaults are conservative; silence reads as dangerous. Design side-effecting tools for the inevitable retry: idempotency keys, read-before-write, dry runs — with approval living in the host, not the tool. Version additively: new optional parameters and new tools, never renames or removals. Deprecation belongs in the description. The seven traps: the helpful description, the mode flag, the mind-reading schema, the silent side effect, the dead-end error, the clever rename, the untested choice. Design exercise Take the http-status server from Chapter 3 and put its tool through the full design checklist: Rewrite the tool description from memory against the five-element checklist. Compare it with the chapter’s version — which element did you drop first? Invent a second tool for the server, e.g. listing codes by category. Argue split versus merge in two sentences, using the flowchart. Write the error message for a category outside the enum. Does it name the value, the rule, and the remedy — and does it say whether retrying helps? Set all four annotations and the title. Then change one fact — the table becomes a live fetch — and list everything that must change with it. Rename lookup http status in your head. List everything that breaks. Now write the deprecation description instead, and notice how much cheaper honesty is than migration. Keep the answers. The chapters ahead put this tool into production settings, where every design choice made here gets tested under real load. Key terms Tool annotation: Optional metadata on a tool definition — readOnlyHint, destructiveHint, idempotentHint, openWorldHint, and title — advising the host about the tool’s behavior. Hints inform confirmation and display; they do not enforce safety. Idempotent tool: A tool for which repeating the call with the same arguments produces no additional effect. Idempotency makes retries safe and should be claimed in annotations only when it is genuinely true. Input schema: The JSON Schema describing a tool’s accepted parameters. The model fills arguments from it, and the server validates against it before the tool runs. Output schema: An optional JSON Schema describing the structure of a tool’s result, letting clients validate responses and models parse them reliably. Side effect: Any change a tool makes beyond returning a value — writing data, calling external systems, sending notifications. Side effects determine approval needs and must be declared, never silent. Version note: This chapter targets MCP specification revision 2026–07–28. Tool names should follow the spec’s naming guidance 1–128 characters, case-sensitive, limited character set . Annotations are advisory hints for hosts — enforce safety in your server code, not in metadata. Check the current specification before treating any detail as settled. Next: Chapter 5 — Resources and Prompts: when the model should read instead of act. Follow to get it when it drops. Designing Tools Well was originally published in Towards AI on Medium, where people are continuing the conversation by highlighting and responding to this story.