All posts
MCP education··4 min read

MCP error handling patterns that hosts can actually use

MCP errors are interface, not exception. Return structured errors with three machine-readable codes the model can act on: retryable (transient infra, host may retry once), user_action_required (auth, scope, quota — host surfaces to user), and permanent (validation, not found, forbidden — don't retry). Use the shape { ok: false, code, message, hint? } where message is human, code is machine, and hint tells the model what to try next. Never leak stack traces — log them server-side and return clean codes.

⌘
Command+K team
Platform engineering

Three categories the model understands

  • retryable — transient (timeout, 5xx upstream). Host may retry once.
  • user_action_required — auth expired, missing scope, quota reached. Host surfaces to user.
  • permanent — validation failure, not found, forbidden. Don't retry.

Error shape

Return { ok: false, code, message, hint? }. Keep message human; keep code machine-readable; use hint to tell the model what to try next (e.g. "ask the user to reconnect Linear").

Never leak stack traces

Stack traces in MCP responses end up in screenshots and support tickets. Log them server-side; return a clean code and message.

Audit every error

Errors are signal. Log code, tool, user_id, and input hash. A spike in user_action_required on one tool usually means a scope or token issue you can fix once for everyone.

FAQ

Should I throw or return errors?
Return structured errors. Throwing crashes the transport; returning gives the host a chance to surface or retry.
What does the model need in an error?
A short message it can show, a machine-readable code (retryable, user_action_required, permanent), and optionally a hint at what to try next.
Retry on the server or the host?
Transient infra errors: retry server-side with jitter, cap at 2. Auth and quota errors: never retry server-side — return and let the host handle.

Keep reading

Go deeper