Of the three primitives an MCP server can expose, tools are the one the model drives. Resources are pulled in by the application; prompts are chosen by the user; tools are selected and invoked by the language model in the middle of its reasoning, on the strength of a name, a paragraph of description, and a JSON Schema. That single fact — the caller is a probabilistic reader of English, not a compiler — is what makes tool design different from API design. This piece walks the machinery end to end: discovery, what a tool definition really contains, who validates arguments, what comes back from a call, side effects and retries, tool lists that change at runtime, and how to shape a surface a model can navigate.
Tools are model-invoked, and that changes everything
MCP separates its primitives by who is in control. A resource is application-controlled: the host decides what context to attach. A prompt is user-controlled: a person picks it from a menu. A tool is model-controlled: the host hands the model a list of what is available and the model decides, unprompted, that this is the moment to call search_issues.
The consequence is that a tool has two audiences with incompatible tastes. The runtime audience is a machine needing an exact name, a well-formed argument object, and a deterministic response. The selection audience is a model that has never seen your system, reading your text once, mid-context, alongside forty other tools. Everything downstream follows from serving both: the schema exists for the machine, the prose for the model, and a tool that satisfies only one fails in a way that looks like ‘the AI is bad at this’ when it is a documentation bug.
tools/list: discovery, and what a tool definition contains
A server that offers tools declares that capability during the initialize handshake, and the client then calls tools/list to find out what exists. The response is an array of tool definitions, paginated with an opaque cursor rather than one unbounded blob.
Each definition carries three load-bearing parts. The name is the stable identifier the model emits verbatim when it calls the tool — an identifier, not a label, so renaming it is a breaking change. The description is free text explaining what the tool does and when to reach for it. The input schema is a JSON Schema object describing the arguments. Current revisions also allow a human-facing title, an optional schema for structured output, and an annotations object of behavioural hints. The client translates this array into whatever tool-calling format its model provider expects — which is why the definition has to stand on its own.
The description is prompt surface, not documentation
This is the most under-appreciated fact about MCP tools: the description is not a README a curious developer might read. It is injected into the model's context on every turn the tool is available. It is prompt engineering, billed at prompt-token rates, and it is the primary evidence the model uses to choose between your tool and a similar one.
Write it accordingly. State what the tool does in a first sentence that stands alone. Say when it should be used and when it should not — negative guidance (‘get_user for a known ID, search_users for a name’) resolves far more misfires than another paragraph of features. Name the units, formats, and defaults, because the model cannot guess whether a timestamp is epoch seconds or ISO-8601. And keep it tight: every description is permanently resident context, so a hundred verbose tools crowd out the conversation.
Input schemas and the two-sided validation contract
The input schema is a JSON Schema object — typically type: "object" with a properties map and a required array. It does double duty: mechanically it constrains arguments, rhetorically it teaches. Per-property description strings, enum values for closed sets, format hints, and sensible defaults all reach the model and measurably improve argument quality. An enum of five allowed statuses eliminates the invented values that free-form strings invite.
Validation sits on both sides. A good client validates the model's proposed arguments before sending, so a malformed call fails fast and is repaired locally rather than burning a round trip. But the server must validate too, and must never treat client-side validation as a security boundary: the arguments arriving over the wire were composed by a language model that may have been influenced by untrusted content. Schema conformance is a correctness check, not an authorization check; the server still owns permissions and sanitization.
tools/call and the shape of a result
Invocation is a tools/call request carrying the tool's name and an arguments object. The server executes and replies with a result whose central field is content: an ordered array of typed content blocks, not a single string.
That array is deliberately multi-modal. A block can be text, an image, audio, or an embedded resource carrying a URI alongside its contents so the host can track provenance. Current revisions also allow a machine-readable structured payload alongside the content blocks for tools that declare an output schema — a separate topic, but worth knowing exists. The practical discipline: whatever lands in content enters the model's context verbatim, so a tool that dumps ten thousand rows of JSON has not returned data, it has destroyed the context window.
Failure arrives on two channels. A JSON-RPC error means the call failed; an ordinary result carrying isError: true means the tool ran and the operation failed. That taxonomy belongs to the error-handling article, and the only consequence a tool author needs here is that the second kind is output — it lands in the model's context like any other content block, so write it as something the model can act on.
Side effects, idempotency, and retry safety
A tool call is an RPC over an unreliable link with an unpredictable caller, which puts side-effect semantics in the design brief. Ask, for every tool, what happens if it runs twice. A search running twice wastes tokens. A send_invoice running twice is a real problem, and the double call is not hypothetical: a timeout leaves the client unsure whether the work happened, and agent loops re-attempt steps they believe failed.
Design for that rather than hoping. Make reads naturally repeatable. For writes, prefer operations that converge — setting a field to a value is safely repeatable in a way that incrementing it is not — or accept a caller-supplied idempotency key and deduplicate server-side. Where an operation cannot be made repeatable, say so in the description. MCP additionally lets a server declare these properties as behavioural hints so clients can build retry and consent policy on top; that vocabulary is covered in the tool annotations article.
Tool lists are not static
It is tempting to treat tools/list as a one-time startup call. It is not. A server that says its tool list may change emits a list-changed notification when it does, and the client re-lists — the notification mechanics are covered elsewhere. What matters for tool design is why a surface moves.
A tool surface is frequently a function of state. A server may expose only read tools until the user authenticates, then reveal write tools — the same token that authorizes the session can determine which tools appear at all, linking authorization to the visible surface. A server scoped to workspace roots may offer different tools depending on what is open. Two consequences follow. Clients must never hard-code a list or assume a cache is fresh, and must handle a call to a tool that has since vanished. And servers should change the list only for real reasons: an agent three steps into a plan built around the tools it saw does not gracefully replan when one disappears underneath it, so churn in the surface reads to the model as the world becoming unreliable.
Naming and granularity: designing for a probabilistic caller
Names are chosen by humans and typed by models, so they should read as verbs on concrete objects: create_issue, list_pull_requests, cancel_deployment. Consistency beats elegance — if half your tools say get_ and half say fetch_, the model will guess wrong. Avoid names that differ by one word from a sibling unless the descriptions make the boundary unmistakable.
Granularity is the harder call. Too fine, and a simple task becomes a six-call chain where every step can go off the rails. Too coarse, and you get a manage_everything tool with a mode flag and twenty conditionally-relevant parameters no model fills in correctly. The reliable heuristic is to model tools on user intentions rather than on your internal API endpoints. If a common task makes the model chain four calls and thread IDs between them, that chain is a candidate for one tool doing the whole job — your server makes those four internal calls far more reliably than the model can.
Too many tools: the surface is a context budget
The failure mode that surprises teams most is that adding tools makes an agent worse. Every definition consumes context on every turn, and every near-duplicate raises the chance the model picks the wrong one. A server that mechanically exposes two hundred REST endpoints as two hundred tools has produced an unusable surface, however complete.
Treat the tool list as a curated menu with a real budget. Expose the operations that matter for the workflows you target, and let the long tail live behind a general escape hatch or not at all. Where a host connects many servers at once, filtering becomes the client's job. The test to apply is not ‘does this cover the API?’ but ‘could a competent person, handed only these names and descriptions and no other knowledge of the system, pick the right one every time?’ If not, the model will not either.
tools/list, the model chooses one from a name, a description and a JSON Schema, and the client issues tools/call to get back an array of typed content blocks. The schema serves the machine and the prose serves the model: the description is genuine prompt surface, resident context that decides whether the right tool gets picked. Validate on both sides, and treat neither the model nor the client as a security boundary. Write execution failures as recovery instructions, since they return as results the model can act on. Assume calls can repeat, and make writes repeatable or deduplicated. And treat the surface itself as a budget: fewer, better-named, intention-shaped tools beat exhaustive coverage every time.