# Error Handling Patterns for External API Calls in MCP Tools: A Production Guide

> Master error handling patterns for external API calls in MCP tools. Learn to gracefully manage network failures and HTTP errors with structured exception handling, async clients, and normalized responses.

- Repository: [CSK/mcp-wordle-python](https://github.com/cr2007/mcp-wordle-python)
- Tags: best-practices
- Published: 2026-02-28

---

**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`](https://github.com/cr2007/mcp-wordle-python/blob/main/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`](https://github.com/cr2007/mcp-wordle-python/blob/main/src/mcp_wordle/main.py) performs a bare HTTP request without defensive programming:

```python
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 `async` but uses the blocking `requests` library, causing performance bottlenecks when handling concurrent tool invocations.
- **No network exception handling**: DNS failures, connection timeouts, or dropped connections raise unhandled `requests.exceptions.RequestException` errors that crash the tool.
- **Unhandled HTTP errors**: Non-2xx status codes from the NYTimes API (such as `404` for invalid dates or `500` for server errors) pass silently, potentially causing `JSONDecodeError` when parsing error HTML as JSON.
- **Excessive timeouts**: The `300` second timeout risks hanging the MCP server indefinitely during network partitions.
- **Unsafe JSON parsing**: Malformed responses trigger uncaught `ValueError` exceptions 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:

```python
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`](https://github.com/cr2007/mcp-wordle-python/blob/main/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:

```python
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:

```python
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:

```python
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 containing `id`, `solution`, `print_date`, `days_since_launch`, and `editor` fields.
- `WordleError`: Error envelope with `status`, `errors` list, and empty `results` array.

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`](https://github.com/cr2007/mcp-wordle-python/blob/main/src/mcp_wordle/main.py) incorporating all error handling patterns:

```python
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:

1. **Retry logic with exponential backoff**: Use `tenacity` to automatically retry transient failures:

```python
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)

```

2. **Structured logging**: Capture full stack traces using `mcp.logger` or standard library `logging` before returning sanitized error messages to clients.

3. **Circuit breakers**: Implement `pybreaker` patterns to stop hammering failed external services during outages, returning cached or degraded responses instead.

4. **Schema validation**: Validate JSON payloads against Pydantic models or `jsonschema` before returning them as `WordleAPIData`, ensuring type safety across the API boundary.

5. **Timeout tuning**: Reduce the client timeout from `300` seconds to `30` seconds (or lower) based on the NYTimes API's observed latency characteristics to fail fast during degradation.

## Summary

- **Never use blocking HTTP clients** like `requests` inside async MCP tool functions; prefer `httpx.AsyncClient` or `aiohttp`.
- **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 predictable `status` field.

## 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`](https://github.com/cr2007/mcp-wordle-python/blob/main/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`](https://github.com/cr2007/mcp-wordle-python/blob/main/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.