Skip to content
Pocket Grove
Developer blog

Write Tool Contracts That Prevent Avoidable Retries

Published 24 September 2026 · 8 min read

By · Pocket Grove

Original research brief: . Sources reviewed for publication.

Quick answer: Put known input limits in the tool schema the agent actually receives, preserve them through adapters, and enforce them again on the server. Keep schema validity, provider generation constraints, and permission checks separate. When a request fails, return enough information to choose a useful next action.

Imagine an agent calling a list tool with a page size of 100. The server replies that the maximum is 50. The agent changes the number and calls again.

That is a plausible recovery. It is also a round trip spent discovering a fact the server already knew. If the tool advertises only “page size: integer,” the caller cannot read its actual boundary from the schema.

The following list_examples design makes that boundary explicit. It is an illustrative contract, with expected outcomes rather than executed test results. It demonstrates what to specify; it does not establish a measured reduction in model retries.

Start with the schema the consumer receives

Suppose a read-only tool lists examples from the caller's current workspace. It accepts between 1 and 50 items per page and an optional continuation cursor.

This property describes too little:

"limit": { "type": "integer" }

Here is a proposed MCP tool definition with the missing contract included:

{
  "name": "list_examples",
  "description": "List examples in the authenticated workspace, ordered by ID. Request 1–50 items. Omit cursor or use null for the first page; otherwise pass next_cursor unchanged from the previous result.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "limit": {
        "type": "integer",
        "minimum": 1,
        "maximum": 50
      },
      "cursor": {
        "type": ["string", "null"],
        "description": "Opaque continuation token for this workspace."
      }
    },
    "required": ["limit"],
    "additionalProperties": false
  }
}

The range includes both endpoints: 1 and 50 satisfy these numeric bounds. 0 and 51 do not. Those are ordinary JSON Schema numeric constraints.

The cursor has a different job. Its schema permits a string or null; the description tells the caller how to obtain and use it. Neither establishes that a supplied token exists.

The MCP tools specification dated 2026-07-28 defines inputSchema and defaults to JSON Schema 2020-12 when no dialect is declared. That describes the protocol contract. It does not establish what a particular model provider can enforce during generation.

When reviewing a real integration, inspect the emitted tool definition and the final provider request. A correct declaration in a server library is only the first checkpoint. A converter might drop a constraint, change a type, or handle optional fields differently. The useful review question is: can the consumer still see the rule?

Separate the three validation layers

Keep a short record of who enforces each part of the contract:

Layer What it establishes What still needs checking
Published schema The declared argument structure and limits Whether adapters preserve them
Provider generation Constraints supported by that model and API path Request completion, semantics, and execution
Server validation Accepted arguments, current permissions, and domain rules Whether the result satisfies the user's task

For example, OpenAI's function calling guide documents strict mode with every property required and additionalProperties: false on each object. Optional values can use a nullable type.

For that path, map inputSchema into the function's parameters, explicitly enable strict: true, and change the required list to ["limit", "cursor"]. The first-page call then includes "cursor": null. Keep the server's meaning consistent: absent or null both mean “start at the beginning.” This is an adapter decision to document, rather than an assumption that every API accepts the same wrapper.

OpenAI's supported-schema documentation includes numeric bounds but excludes some composition keywords, including if, then, and else. Fine-tuned models have further restrictions, including numeric bounds. Check the selected model and API path before sending the schema.

If a constraint is unavailable there, retain server enforcement and describe the limit clearly. Record the loss of generation enforcement. A silently weakened schema leaves reviewers unable to tell which promises remain effective.

A valid cursor can still be the wrong cursor

Now consider this input:

{ "limit": 25, "cursor": "example-token" }

It fits the proposed schema. It should still fail if the token is unknown, expired, or belongs to a workspace the caller cannot access. These are checks against current application state.

For this design, resolve the workspace from authenticated context, validate the cursor's association with it, and check access on every request. Do not let the mere possession of a well-shaped string select another customer's records.

The contract should also say what pagination means when data changes. Does the cursor continue through a fixed snapshot, or through a live collection that may shift between calls? Both are possible designs. A caller trying to enumerate every example needs to know which it has.

Here is a review matrix for the proposed inputs. These are expected outcomes, not observations from a running server:

Input Expected decision
limit: 0 or limit: 51 Reject during server schema validation
limit: 1 or limit: 50 Pass the numeric check; continue authorization and cursor checks
Missing limit Reject: required property absent
Unknown property, such as include_private Reject: extra properties forbidden
Missing cursor or null Begin the first page after authorization
Cursor with the wrong JSON type Reject during schema validation
Unknown, expired, or inaccessible cursor string Reject during domain or access validation

A provider may prevent some invalid arguments from being generated. The server still owns the acceptance decision for every caller.

Make an error useful to the next decision

“Invalid arguments” leaves too much work to the caller. A bounded-input error can identify the field and the permitted range without exposing internal data.

For example, this is an illustrative application error payload, not a complete MCP response:

{
  "code": "INVALID_LIMIT",
  "field": "limit",
  "message": "limit must be an integer from 1 through 50.",
  "retry_same_request": false
}

The last field means repeating unchanged arguments will not help. A corrected request may succeed. An expired cursor needs a different recovery: restart pagination if the task permits it. An access denial needs an authorized route, rather than another guessed identifier.

MCP distinguishes malformed protocol requests from tool execution errors; its 2026-07-28 tools specification includes input validation failures in the latter and uses isError: true. Map application errors into the appropriate response for the protocol version in use. MCP error handling provides the transport-facing distinction.

Keep sensitive details out of those messages. Explain what the caller can do without confirming another tenant's records exist.

Specify the result as carefully as the request

For list_examples, define success as an object containing items and next_cursor. Specify the fields in each item, a maximum page size, and that next_cursor: null means enumeration has ended. Document ordering and cursor lifetime alongside that shape.

Returning fewer items than requested need not mean the end. Let the continuation field carry that decision. Also check that the server never returns more items than the requested limit; that relationship is part of this application's contract.

MCP supports an optional outputSchema for structured results. Server-produced structured content is distinct from schema-constrained model generation. MCP output schemas describe that result boundary.

A tool contract review checklist

Copy this into a tool's review notes:

  • Inputs: Required fields, nullable values, unknown fields, numeric limits, and units are explicit.
  • Delivery: The final emitted schema retains the intended constraints after conversion.
  • Provider: Supported keywords and strict-mode settings match the selected API path.
  • Server: Arguments are validated independently of how they were generated.
  • Access: Workspace selection, authorization, and token scope are checked during execution.
  • Recovery: Each error tells the caller whether to correct, restart, wait, or stop.
  • Results: Item shape, continuation, ordering, and consistency have defined meanings.
  • Evidence: Expected behavior is distinguished from actual checks and measured model outcomes.

Where practical, generate the schema and validator from one definition. Review the generated boundary cases as well: shared source cannot prevent a converter from losing information.

The same boundary appears in our discussion of Jev and typed AI decisions: software must decide what an answer permits it to do. Tool contracts make that responsibility concrete. Use task-based agent evaluations when you want to measure the effect, and keep reusable contract guidance discoverable in your agent knowledge base.

Start with one tool whose callers repeatedly hit a known limit. Follow that limit from its source definition to the consumer request and server check. Make every boundary agree before asking the model to compensate for missing information.

Apps from the studio

All apps

These practices come from shipping Pocket Grove's active apps. If you came here looking for something to install, start with one of these.

Related guides