← Back to Build

Error Handling

Help agents recover instead of failing silently. Good error handling is the difference between a retry and a dead end.

Why This Matters

When an MCP tool call fails, the AI agent reads the error message to decide what to do next. A good error message lets the agent self-correct — fix the argument and retry. A bad error message (or worse, a silent failure) leaves the agent stuck, burning tokens on blind retries or giving up entirely.

Key insight: Error messages are instructions for the AI. Write them the same way you'd write tool descriptions — clear, actionable, and specific.

Two Layers of Errors

MCP has two distinct error layers. Understanding the difference is critical:

1. Protocol-Level Errors (JSON-RPC 2.0)

These are transport/protocol failures — the request itself was malformed, the method doesn't exist, or the server hit an internal error. They use standard JSON-RPC 2.0 error codes and go in the error field of the response.

CodeNameWhen to Use
-32700Parse errorInvalid JSON received
-32600Invalid requestJSON is valid but not a proper JSON-RPC request
-32601Method not foundThe method (e.g., tools/call) doesn't exist
-32602Invalid paramsMethod exists but parameters are wrong
-32603Internal errorUnexpected server-side failure

2. Tool-Level Errors (Application Errors)

These are business logic failures — the tool was called correctly at the protocol level but couldn't complete the operation. These use the isError: true flag in the tool result, not JSON-RPC error codes.

Critical distinction: A "user not found" error is a tool-level error (isError: true in the result). A "missing required parameter" error is a protocol-level error (JSON-RPC -32602). Don't confuse them.

Returning Tool Errors

When a tool encounters a business logic error, return it as a successful JSON-RPC response with isError: true in the result content:

{
  "content": [
    {
      "type": "text",
      "text": "User with email 'bad@example.com' not found.
Try using search_users to find the correct email."
    }
  ],
  "isError": true
}

What Makes a Good Error Message

  1. State what went wrong — "User not found" is better than "Error"
  2. Include the failing input — "No user with email 'bad@example.com'" helps the agent understand what to fix
  3. Suggest what to do next — "Try search_users to find the correct email" gives the agent a recovery path
  4. Never return empty errors{"isError": true} with no message is the worst possible outcome

Error Message Pattern

Follow this three-part structure for every tool error:

[What failed]: [specific details about the failure]
[Recovery suggestion or next step]

Examples:

  • "Repository 'acme/missing-repo' not found. Check the owner/repo format and verify the repository exists."
  • "Rate limit exceeded (429). Retry after 30 seconds or reduce the 'limit' parameter."
  • "Invalid date format '03-2026'. Expected ISO 8601 format, e.g. '2026-03-01'."

Handling Common Error Scenarios

Missing or Invalid Parameters

Validate inputs before doing any work. Return specific messages about which parameter failed and what was expected:

  • "Parameter 'email' is required but was not provided."
  • "Parameter 'limit' must be between 1 and 100. Received: 500."
  • "Parameter 'status' must be one of: pending, shipped, delivered. Received: 'active'."

External Service Failures

When your tool depends on an external API that fails:

  • Include the HTTP status code: "GitHub API returned 503 Service Unavailable."
  • Suggest a retry if appropriate: "This is likely temporary. Retry in a few seconds."
  • Offer an alternative if one exists: "GitHub API is down. Try using the cached data with get_cached_repos."

Authentication Errors

  • Be specific: "API key is expired or invalid. Re-authenticate with the auth/refresh endpoint."
  • Never leak credentials in error messages
  • Distinguish between "not authenticated" and "not authorized" (permission denied)

Resource Not Found

  • Include the identifier: "Issue #4521 not found in repository 'acme/app'."
  • Suggest discovery tools: "Use list_issues to find valid issue numbers."
  • Never start error messages with "not found" — some clients interpret this as a failed search result rather than an error

Graceful Degradation

When possible, return partial results instead of a full error:

  • If 3 of 5 files were read successfully, return the 3 that worked and note which 2 failed
  • If a search returns no results, say so clearly rather than returning an empty array with no context
  • If a rate limit is approaching, include a warning in a successful response rather than waiting to fail
{
  "content": [
    {
      "type": "text",
      "text": "Read 3 of 5 files successfully.\n\n
Failed:\n- /path/to/missing.txt (file not found)\n
- /path/to/locked.txt (permission denied)\n\n
Successfully read:\n- /path/to/a.txt\n- /path/to/b.txt\n
- /path/to/c.txt"
    }
  ],
  "isError": false
}

What Not to Do

Anti-PatternProblemBetter Approach
Return {"error": "something went wrong"}Agent can't recoverSpecific message with recovery path
Throw unhandled exceptionsCrashes the server, breaks the connectionCatch and return structured errors
Return success with empty resultsAgent thinks it workedReturn isError: true with explanation
Include stack traces in error messagesLeaks internals, wastes tokensLog the stack trace, return a clean message
Use generic HTTP status codesMCP uses JSON-RPC, not RESTUse JSON-RPC 2.0 codes for protocol errors
Silently swallow errorsAgent has no idea what happenedAlways return an error indicator

Edge Case Testing

The MCP Scoreboard tests servers against these edge cases during deep probes. Make sure your server handles them:

  • Missing required parameters — should return -32602 or a clear tool error
  • Wrong parameter types — string where number expected, null in required field
  • Unknown tool names — calling a tool that doesn't exist should return -32601
  • Oversized payloads — extremely long strings, arrays with thousands of items
  • Unicode edge cases — emoji, RTL text, null bytes in strings
  • Concurrent requests — multiple tool calls at the same time
  • Empty string arguments"" vs. missing vs. null

Logging Errors

For stdio transport, logging has a critical constraint:

Never log to stdout. Any output to stdout corrupts the JSON-RPC stream. Use console.error (stderr) for logging, or use a file-based logger. This is the #1 cause of mysterious connection failures in stdio servers.
  • Log every error with enough context to debug: tool name, arguments (redacted if sensitive), error details
  • Include request IDs if available for correlation
  • Log at appropriate levels: WARN for recoverable issues, ERROR for failures

Quick Checklist

  • Protocol errors use standard JSON-RPC 2.0 error codes
  • Tool errors use isError: true in the result content
  • Every error message states what went wrong and what to do next
  • Error messages include the failing input value
  • No empty or generic error messages
  • No stack traces or internal details in error responses
  • Partial results are returned when possible instead of full failures
  • All inputs are validated before processing
  • External service failures include status codes and retry guidance
  • Logging goes to stderr (not stdout) for stdio transport
  • Edge cases are tested: wrong types, missing params, unicode, oversized payloads

Essential Resources

Official Specification

Error Handling Guides

Transport & Debugging

Production Patterns

SDKs & Frameworks