Error Handling Patterns for External API Calls in MCP Tools: A Production Guide
Production-ready MCP tools require structured exception handling, async HTTP clients, and normalized error responses to gracefully manage network failures, HTTP errors, and malformed payloads from external APIs.
The cr2007/mcp-wordle-python repository demonstrates a common anti-pattern in MCP tool development: direct API calls without resilience mechanisms. While the current implementation in src/mcp_wordle/main.py fetches Wordle data using synchronous requests, production environments demand robust error handling patterns for external API calls in MCP tools to prevent cascading failures and provide predictable error contracts to downstream consumers.
Why the Current Implementation Is Fragile
The existing code at lines 64-66 of src/mcp_wordle/main.py performs a bare HTTP request without defensive programming:
url = f"https://www.nytimes.com/svc/wordle/v2/{target_date}.json"
return requests.get(url, timeout=300).json()
This implementation contains multiple failure points that violate reliability best practices:
- Blocking the event loop: The function is declared
asyncbut uses the blockingrequestslibrary, causing performance bottlenecks when handling concurrent tool invocations. - No network exception handling: DNS failures, connection timeouts, or dropped connections raise unhandled
requests.exceptions.RequestExceptionerrors that crash the tool. - Unhandled HTTP errors: Non-2xx status codes from the NYTimes API (such as
404for invalid dates or500for server errors) pass silently, potentially causingJSONDecodeErrorwhen parsing error HTML as JSON. - Excessive timeouts: The
300second timeout risks hanging the MCP server indefinitely during network partitions. - Unsafe JSON parsing: Malformed responses trigger uncaught
ValueErrorexceptions rather than structured error responses.
Core Error Handling Patterns for MCP Tools
Robust MCP implementations follow a layered defense strategy that validates every stage of the HTTP transaction.
Use Async HTTP Clients
Replace requests with httpx.AsyncClient to maintain non-blocking I/O throughout the MCP tool execution:
import httpx
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.get(url)
This change aligns the HTTP client with the async function signature in main.py, preventing event loop starvation when multiple tools run concurrently.
Catch Network-Level Exceptions
Wrap the request in a try/except block targeting httpx.RequestError to handle DNS failures, SSL issues, and connection timeouts:
except httpx.RequestError as exc:
return {
"status": "error",
"errors": [f"Network error: {str(exc)}"],
"results": []
}
This pattern ensures that transient infrastructure issues return controlled error payloads rather than stack traces.
Validate HTTP Status Codes
Always inspect the response status before parsing content. Use manual validation to return context-specific error messages:
if resp.status_code != 200:
return {
"status": "error",
"errors": [f"HTTP {resp.status_code} – {resp.text.strip()}"],
"results": []
}
Alternatively, resp.raise_for_status() propagates HTTP errors as exceptions that you can catch and map to the WordleError TypedDict structure.
Implement Safe JSON Parsing
Protect against schema changes or corrupted responses by catching parsing exceptions:
try:
data = resp.json()
except ValueError:
return {
"status": "error",
"errors": ["Unable to decode JSON from Wordle API"],
"results": []
}
This prevents JSONDecodeError from propagating to MCP clients when the external API returns HTML error pages or unexpected content types.
Normalize Return Types
The repository defines two TypedDict schemas that enforce consistent return shapes:
WordleAPIData: Success payload containingid,solution,print_date,days_since_launch, andeditorfields.WordleError: Error envelope withstatus,errorslist, and emptyresultsarray.
Structure the tool to return Union[WordleAPIData, WordleError] so downstream consumers can reliably check if response["status"] == "error" without additional type inspection.
Production-Ready Implementation for Wordle MCP
Below is the complete refactored implementation for src/mcp_wordle/main.py incorporating all error handling patterns:
import httpx
from typing import TypedDict, Union
from datetime import date
from fastmcp import FastMCP
mcp = FastMCP("WordleMCP")
class WordleAPIData(TypedDict):
id: int
solution: str
print_date: str
days_since_launch: int
editor: str
class WordleError(TypedDict):
status: str
errors: list[str]
results: list
@mcp.tool(
name="get_wordle_solution",
description=(
"Fetches the Wordle of a particular date provided "
"between 2021-05-19 to 23 days future"
),
annotations={"readOnlyHint": True},
)
async def get_wordle_data(
target_date: str = date.today().isoformat(),
) -> Union[WordleAPIData, WordleError]:
"""
Retrieves Wordle puzzle data for a specific date with robust error handling.
"""
url = f"https://www.nytimes.com/svc/wordle/v2/{target_date}.json"
try:
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.get(url)
if resp.status_code != 200:
return {
"status": "error",
"errors": [f"HTTP {resp.status_code} – {resp.text.strip()}"],
"results": [],
}
try:
data = resp.json()
except ValueError:
return {
"status": "error",
"errors": ["Unable to decode JSON from Wordle API"],
"results": [],
}
return data
except httpx.RequestError as exc:
return {
"status": "error",
"errors": [f"Network error: {str(exc)}"],
"results": [],
}
This implementation replaces lines 64-66 of the original file, ensuring that every execution path returns either a valid WordleAPIData object or a structured WordleError response.
Advanced Resilience Strategies
Beyond basic exception handling, production MCP tools should implement additional safeguards:
- Retry logic with exponential backoff: Use
tenacityto automatically retry transient failures:
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10)
)
async def fetch_with_retry(client, url):
return await client.get(url)
-
Structured logging: Capture full stack traces using
mcp.loggeror standard libraryloggingbefore returning sanitized error messages to clients. -
Circuit breakers: Implement
pybreakerpatterns to stop hammering failed external services during outages, returning cached or degraded responses instead. -
Schema validation: Validate JSON payloads against Pydantic models or
jsonschemabefore returning them asWordleAPIData, ensuring type safety across the API boundary. -
Timeout tuning: Reduce the client timeout from
300seconds to30seconds (or lower) based on the NYTimes API's observed latency characteristics to fail fast during degradation.
Summary
- Never use blocking HTTP clients like
requestsinside async MCP tool functions; preferhttpx.AsyncClientoraiohttp. - Always catch network exceptions (
httpx.RequestError) separately from HTTP logic errors to provide actionable error messages. - Validate HTTP status codes before attempting JSON parsing to avoid decoding errors on HTML error pages.
- Return normalized error types using TypedDict schemas (
WordleError) so MCP clients can handle failures programmatically. - Structure tools to return Union types that clearly distinguish between success (
WordleAPIData) and error states through a predictablestatusfield.
Frequently Asked Questions
What exceptions should MCP tools catch when calling external APIs?
MCP tools should catch network-level exceptions like httpx.RequestError (or aiohttp.ClientError) separately from application-level errors like HTTP 4xx/5xx responses and JSON parsing failures. According to the cr2007/mcp-wordle-python source code, wrapping the entire request lifecycle in a try/except block and returning a structured WordleError response prevents unhandled exceptions from crashing the MCP server.
Should MCP tools use synchronous or asynchronous HTTP clients?
Always use asynchronous clients such as httpx.AsyncClient when the tool function is declared with async def. Synchronous libraries like requests block the Python event loop, preventing other MCP tools from executing concurrently and degrading overall server throughput. The original implementation in src/mcp_wordle/main.py violated this pattern, causing potential performance bottlenecks.
How should MCP tools structure error responses?
MCP tools should return consistent error envelopes that match the schema expectations of downstream consumers. The Wordle MCP example uses WordleError TypedDict containing status: "error", an errors list with human-readable messages, and an empty results array. This normalization allows client code to check if response.get("status") == "error" without inspecting exception types or HTTP status codes directly.
What timeout values are appropriate for MCP tool API calls?
Set timeouts based on the Service Level Objectives (SLOs) of the external API and your MCP server's responsiveness requirements. The original 300 second timeout in main.py is excessive for a simple JSON fetch; 30 seconds provides adequate headroom for the NYTimes Wordle API while ensuring the tool fails fast enough to prevent resource starvation. For interactive MCP tools, consider 10 second timeouts to maintain responsive user experiences.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →