An MCP tool should distinguish a successful search with no matches from a request that needs correction and a service that could not answer. Those outcomes require different next steps. Returning the same empty list or generic error for all three makes it harder for an assistant to explain what happened and decide whether to retry.

This guide uses a fictional support-ticket search tool. The examples are proposed response designs, not a customer deployment or measured evaluation. The aim is a small, explicit contract that your service, client application, and assistant can interpret consistently.

Separate protocol failures from tool execution failures

The November 25, 2025 MCP tools specification distinguishes protocol errors, such as an unknown tool or a malformed request, from tool execution errors such as input validation, API failures, and business-rule failures. Execution errors are returned in a tool result with isError: true, allowing clients to expose useful feedback to the model. See the MCP tools error-handling specification.

For our example, search_tickets is a known tool. A call that reaches it with an unsupported status value should identify that input problem. A request naming a tool that does not exist belongs to the protocol layer. Keep those cases separate in tests and monitoring; they point to different fixes.

Transport authorization also has its own handling. A broken connection or an expired access token should not be disguised as a successful search. Our remote MCP authorization guide explains that boundary.

Treat an empty search as a valid answer

Suppose a user asks for open tickets in their authorized workspace. The service executes the query successfully and finds none. Our recommended application result is a successful, complete search with an empty collection:

{
  "status": "complete",
  "tickets": [],
  "has_more": false
}

This object is illustrative application data, not a full MCP response. Its field names are a proposed contract, not standard MCP fields. The surrounding tool result should not mark this completed search as an execution error.

The assistant can now say that no tickets matched the specified filters in the accessible workspace. It should not broaden that into a claim that no tickets exist anywhere, nor imply that an unavailable system was successfully checked.

Define completeness carefully. If only one page or one data source was searched, reflect that limit in the result. If a dependency failed halfway through, an empty list alone is misleading. Either return a failure or an explicitly documented partial outcome; do not silently present incomplete coverage as complete.

Give invalid input a specific correction

Now suppose the caller submits status: "urgent", while this fictional tool accepts only open or closed. “Something went wrong” leaves the assistant guessing. A more useful execution-error result is:

{
  "isError": true,
  "content": [
    {
      "type": "text",
      "text": "Invalid status. Use open or closed."
    }
  ]
}

This is the MCP tool-result portion, without the JSON-RPC envelope. The wording is original to this example. It explains which input failed and the allowed alternatives without implying that either alternative matches the user's intent.

For a request about urgent tickets, the right next step may be to clarify whether the service has a priority filter. It is not necessarily to substitute open automatically. Error feedback should help correct the request while preserving the user's meaning.

Put allowed values and field meaning in the input schema and description too. Our tool description guide covers that work. Runtime validation remains necessary even when the schema is clear.

Preserve uncertainty when the service cannot answer

If the ticket database is unavailable, the service has not established whether matching tickets exist. Our proposed user-facing message is: “Ticket search is temporarily unavailable. No search results were obtained.” That is materially different from “No tickets found.”

For a structured application error contract, you might define a stable code such as UPSTREAM_UNAVAILABLE, a safe message, and a retry policy. Name these fields in your own documentation and implement their handling in the client; adding a field called retryable does not make every MCP client obey it.

MCP supports structured tool-result data, and structured results must conform when an output schema is provided. Design that schema to cover the structured outcome variants your tool actually returns. The specification also recommends a serialized JSON text block for backward compatibility when returning structured content. See MCP structured content and output schemas.

Keep the structured and text versions consistent. A machine-readable error paired with reassuring success text creates two competing accounts of the same request.

Make retries a policy, not a reflex

For this read-only search, a short transient outage may justify a bounded retry. An invalid status value needs a corrected input. A permission denial needs the appropriate access decision, not repeated attempts with different identifiers.

If an upstream HTTP service supplies Retry-After, interpret it according to that service's contract. HTTP defines it as guidance on how long to wait before a follow-up request; with a 503 response it indicates the expected period of unavailability. It is not a guarantee that the next call succeeds. See RFC 9110, section 10.2.3.

Our recommendation is to establish a maximum attempt count, an overall time budget, and a clear stopping response in the client or orchestration layer. Waiting longer must not turn a request into an unbounded background task the user never agreed to.

Write actions require additional care. If a booking request times out after the service may have created a reservation, “try again” is incomplete advice. Preserve the operation identifier and use the established recovery path, as described in our idempotency guide. An error message cannot establish that a write had no effect.

Expose useful feedback without exposing private details

Return enough information to choose the next step, while keeping secrets and internal diagnostics out of model-visible results. In this example, a support reference can help an operator locate the failure without returning a database connection string, a raw upstream body, or another workspace's name.

Apply the same access policy to error text as to successful data. “Ticket belongs to Company B” can disclose information even when the ticket body is withheld. For inaccessible records, use your service's documented disclosure policy consistently across search and direct lookup.

Treat upstream error messages as untrusted data. Map them into a small set of known outcomes rather than blindly forwarding arbitrary text as guidance. The service should decide what an error means and which recovery options it supports.

Test the next action as well as the response

Prepare synthetic fixtures and define expected behaviour before release:

Verify both direct tool calls and representative assistant conversations. Direct tests establish the server contract; conversation tests reveal whether the client and assistant use it sensibly. Record actual outcomes without treating a few successful examples as a reliability guarantee.

Make the response contract part of the tool scope

For each proposed tool, write down what success, no result, invalid input, unavailable service, and uncertain completion mean. Then specify the next action each outcome permits. That small document is often more useful than a long list of generic error strings.

If you are still selecting your first capabilities, the Agent Readiness Audit can help frame a tool scope from API documentation. Confirming error behaviour requires implementation and testing of the real service, including its failure paths.