← Back to Blog

Errors an agent can fix: the ToolError discipline

·5 min read

Most MCP servers treat errors as an afterthought: raise something, let the framework stringify it. But the consumer of a local development server is a model deciding what to do next, and the difference between progress and abandonment is usually one actionable sentence.

The taxonomy we adopted

The official SDK draws the line we wanted. Raising ToolError returns is_error true with your message in content: the request succeeded, the tool failed, the model reads why. Anything unexpected becomes a sanitized crash with the traceback confined to logs. The deciding question, straight from the SDK's own guidance: could a smarter model have avoided this? In a development-tool domain, the answer is almost always yes, because the fix is editing a file the agent can edit.

Every error names the exit

  • No pyrpc.json found states the directory searched and prints a minimal valid config inline, plus the pyrpc init alternative.
  • Missing backend section lists the valid frameworks, includes entrypoint syntax per framework, and reports what marker-sniffing detected as a hint, never a selection.
  • Django without types_module quotes the field, explains why manage.py cannot register procedures, and suggests the views module.
  • Failed backend import names the module, the exception class and message, and reminds the agent the process runs in the project environment it was launched from.
  • Unknown procedure enumerates every registered name so the next attempt is spelled correctly.

Sniffing as a hint, never a decision

The wizard in pyrpc init confirms interactive choices; the MCP has no interactively reachable human, so detection degrades to prose. The error may say markers suggest fastapi, and stops there. Silent fallbacks produce confident wrong introspection, which is the worst outcome an agent can be handed. Deterministic resolution plus explicit failure is the contract, and it matches how pyrpc dev --yes already behaves outside the MCP.