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.
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.
| Code | Name | When to Use |
|---|---|---|
| -32700 | Parse error | Invalid JSON received |
| -32600 | Invalid request | JSON is valid but not a proper JSON-RPC request |
| -32601 | Method not found | The method (e.g., tools/call) doesn't exist |
| -32602 | Invalid params | Method exists but parameters are wrong |
| -32603 | Internal error | Unexpected 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.
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
- State what went wrong — "User not found" is better than "Error"
- Include the failing input — "No user with email 'bad@example.com'" helps the agent understand what to fix
- Suggest what to do next — "Try search_users to find the correct email" gives the agent a recovery path
- 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-Pattern | Problem | Better Approach |
|---|---|---|
Return {"error": "something went wrong"} | Agent can't recover | Specific message with recovery path |
| Throw unhandled exceptions | Crashes the server, breaks the connection | Catch and return structured errors |
| Return success with empty results | Agent thinks it worked | Return isError: true with explanation |
| Include stack traces in error messages | Leaks internals, wastes tokens | Log the stack trace, return a clean message |
| Use generic HTTP status codes | MCP uses JSON-RPC, not REST | Use JSON-RPC 2.0 codes for protocol errors |
| Silently swallow errors | Agent has no idea what happened | Always 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
-32602or 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:
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: truein 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
- Tools — MCP Spec (Stable 2025-06-18)
- Transports — MCP Spec (stdio, Streamable HTTP)
- Security Best Practices — MCP Spec
- JSON-RPC 2.0 Specification
Error Handling Guides
- Error Handling in MCP Servers — Best Practices Guide (MCPcat)
- MCP is Not the Problem, It's Your Server (Philipp Schmid)
- Implementing MCP: Tips, Tricks & Pitfalls (Nearform)
- MCP Best Practices: Architecture & Implementation Guide
- 5 Best Practices for Building MCP Servers (Snyk)
Transport & Debugging
- MCP Transport Protocols: stdio vs SSE vs Streamable HTTP (MCPcat)
- MCP Inspector — Debugging Tool
- MCP Server Troubleshooting: Common Errors & Fixes (MCP Playground)
Production Patterns
- 15 Best Practices for MCP Servers in Production (The New Stack)
- Top 5 MCP Server Best Practices (Docker)
- MCP Server Development Guide (cyanheads, GitHub)
- Running Efficient MCP Servers in Production (DEV)