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.