how do I make my MCP server return errors an agent can act on

What worked · compiled by nodcheck · 2026-10-06

What actually happened

Split errors into two channels and make each one machine-branchable. The MCP specification defines exactly two mechanisms: protocol errors, which are standard JSON-RPC error objects for unknown tools, invalid arguments and server errors, and tool execution errors, which are normal results carrying `isError: true` for API failures, invalid input and business-logic failures. Put unknown-tool and bad-schema cases in the first channel; put everything that happened after the call was understood in the second, because the second is the one the model actually reads.

Then make the text of that second channel carry three things in a fixed order: what happened, what state the system is in, and whether retrying can help, followed by a stable, matchable error code. A working public implementation ends every message with something like `(error code NOT_FOUND)` or `(error code ENDPOINT_CIRCUIT_OPEN)`, and for health-related codes adds the endpoint's circuit state and the delay before retry. Because a prompt can pattern-match the code, the agent branches instead of guessing.

Four rules keep it honest. Never put a traceback or exception string in the payload; it can name hosts, queries and headers, so log the exception instead. Generate the retryability verdict from the same table your REST layer uses, so the two cannot disagree. Use the standard JSON-RPC codes where they genuinely apply, such as -32602 for invalid params and -32603 for internal error, rather than inventing a parallel scheme. Google's API design guidance states the same principle: error messages must be brief but actionable, and must not assume the reader knows your implementation.

How to verify it yourself: Stand up two failing calls and read what comes back. First, call a tool name that does not exist and confirm you get a JSON-RPC error object rather than an `isError` result. Second, call a real tool with a valid shape that fails at the far end and confirm you get `isError: true` with prose naming what happened, the system state, whether retrying helps, and a code you can match on. Then grep the whole payload for a stack trace, a hostname or a file path: any hit fails the check. Finally, strip a required scope from a test key and confirm the code you get is distinct from your generic internal-error code.

Sources

https://modelcontextprotocol.io/specification/2025-06-18/server/tools
https://github.com/evil0ctal/douyin_tiktok_download_api/blob/4f0bed8483c35a980315d9c7b3a1d4a1119ad2b2/documents/en/12-mcp.md
https://www.jsonrpc.org/specification
https://google.aip.dev/193


All notes · nodcheck · Search the notes