In this guide
A tool named search accepts a string called query and returns another string. That interface may be easy to implement. It leaves the caller to guess what can be searched, how many results are returned, whether dates are local or UTC, and what an empty response means.
Those guesses become expensive in an agent workflow. A model may repeatedly call the wrong tool or fill missing constraints with plausible values. A human debugging the run then has to infer the intended contract from logs.
A useful tool contract makes valid use specific and failure recoverable. It describes the boundary of an operation, rather than relying on a long description to compensate for an ambiguous interface.
Start with the decision the caller must make
Compare two proposed tools: get_order and search_orders. The first retrieves one known order identifier. The second finds candidates using a constrained filter. Their descriptions should explain when to choose each, including what not to use it for.
For example: “Find orders by customer reference and creation-date range. Returns at most the requested limit and a continuation cursor. Use get_order when you already have an order ID.” This helps the caller distinguish discovery from retrieval without inventing a universal rule about how every model will behave.
Keep authorization facts out of caller-controlled parameters where possible. A tool should not ask the model to provide the tenant it is allowed to search if the server can derive that tenant from the session. The schema describes valid arguments; it does not grant access to the records those arguments identify.
Make the schema carry its share
The MCP tools specification uses JSON Schema for input definitions and distinguishes protocol failures from tool execution errors. In JSON Schema, declaring a property does not automatically make it required; the object reference explains the separate roles of properties, required and additional properties.
A small application-specific search schema might look like this:
{
"type": "object",
"properties": {
"customer_reference": {"type": "string", "minLength": 1, "maxLength": 80},
"limit": {"type": "integer", "minimum": 1, "maximum": 50}
},
"required": ["customer_reference", "limit"],
"additionalProperties": false
}
This is an illustrative schema, not a complete order-search API. Its useful property is that the caller can discover what the server will accept. When the handler validates against this schema, unknown fields fail explicitly instead of being silently interpreted by a downstream helper.
Use bounds that reflect the operation. A maximum string length should be justified by storage, search or interface constraints. Do not choose a large arbitrary number merely to make validation look complete. Apply semantic checks after structural validation: a validly formatted order ID still might refer to an inaccessible order.
Give results a stable shape
Return the fields needed for the next decision, not every field available from the database. For an order search, a result can include an identifier, date, status and a short display label. Detailed addresses or payment information are unnecessary if the next step is simply to choose the right order.
Represent pagination and truncation explicitly. If the server returns the first page of matching orders, say so. An empty list means no matches in the authorized search scope; it should not be confused with a timeout that prevented the search from completing.
Where a client uses structured results, define and validate that result contract as carefully as the input. Maintain a human-readable explanation for inspection, but avoid making callers scrape prose to discover identifiers or retry instructions.
Design an error vocabulary
An error should answer three questions: what failed, whether retrying is sensible, and what change might make the request succeed. The answer should not expose credentials, internal queries or another customer's object names.
| Condition | Helpful outcome | Unhelpful behavior |
|---|---|---|
| Limit outside the supported range | State the valid range | Clamp silently and hide the change |
| No accessible matches | Return an empty successful result | Throw a generic server exception |
| Downstream service temporarily unavailable | Return a bounded retry instruction | Encourage endless immediate retries |
| Caller lacks permission | Explain the permitted next step | Retry using a broader credential |
| Write accepted but result lost | Report an unknown outcome and lookup path | State confidently that nothing happened |
Use stable application error codes where the client needs to branch on behavior. Treat the explanatory text as text for a person or model, not as the only machine-readable discriminator. Keep codes documented and resist reusing one code for unrelated states.
The MCP execution-error mechanism can convey a failed tool operation through isError. A malformed protocol request belongs to a different layer. Conflating them makes recovery harder because the client cannot tell whether to change arguments, reconnect or stop.
Retries need operation semantics
A retry flag alone cannot make a write safe. Imagine reserve_item successfully creates a reservation, then its response is lost. Repeating the call may allocate a second item unless the service enforces deduplication or exposes an operation-status lookup.
Define the idempotency scope with the tool: which principal, resource and argument set does a key cover? What happens if the same key arrives with different arguments? How long is the recorded outcome retained? These are application decisions, and the contract should expose the relevant behavior.
For read operations, retries still need limits. Bound attempts, time and result size. A client should not spend an entire task budget repeatedly calling a search that has already returned a permanent validation error.
Review the contract from a failing caller's position
Test the tool with missing fields, unknown fields, long strings, empty strings, boundary numbers, inaccessible identifiers and stale versions. Inspect the actual error result, not just whether the handler threw an exception.
Then try a normal task using only the public tool description and schema. Can another engineer choose the right tool, predict its side effects and interpret an empty result without reading the implementation? If not, the interface still depends on knowledge the caller does not have.
A good contract does not guarantee a model will always choose correctly. It gives the model, client and operator a shared basis for detecting mistakes and recovering from them. That is a more useful promise than an interface that works only when its author is present to explain it.
Sources and further reading
- MCP tools
Input/output schemas and the distinction between protocol and tool-execution errors.
- JSON Schema object reference
Property validation, required fields and additionalProperties behavior.