npm package: @mcp-utils/retry
Description: Retry with exponential backoff for MCP tool handlers — powered by vurb.
Latest version: 1.0.0

# @mcp-utils/retry

<div align="center">

[![npm](https://img.shields.io/npm/v/@mcp-utils/retry?color=ff4d00&label=npm)](https://npmjs.com/package/@mcp-utils/retry)
[![License](https://img.shields.io/npm/l/@mcp-utils/retry)](LICENSE)

</div>

---

External APIs fail. Networks are unreliable. A single transient error — a 503 from your payment gateway, a database connection blip — is enough to break the entire agent interaction.

LLM agents don't retry automatically. When your tool throws, **the AI gives up**.

Here's how to make your tools resilient in one line:

```ts
import { withRetry } from '@mcp-utils/retry';

// Before: one transient error = broken tool call = broken agent
handler: async (ctx, args) => success(await paymentApi.charge(args))

// After: survives failures, recovers automatically
handler: withRetry(
    async (ctx, args) => success(await paymentApi.charge(args)),
    { attempts: 3, backoff: 'exponential' }
)
```

## Install

```bash
npm install @mcp-utils/retry
```

## Usage

### Exponential backoff (recommended for APIs)

```ts
import { withRetry } from '@mcp-utils/retry';

const chargePayment = withRetry(
    async (ctx, args) => success(await paymentApi.charge(args)),
    {
        attempts: 3,
        backoff: 'exponential', // 300ms → 600ms → 1200ms
        baseDelayMs: 300,
    },
);
```

### Retry only when it makes sense

Don't retry auth failures or business logic errors — only transient failures:

```ts
const fetchData = withRetry(handler, {
    attempts: 5,
    retryOn: (err) => {
        if (err instanceof AuthError) return false;      // don't retry — permanent
        if (err instanceof RateLimitError) return true;  // retry — transient
        return isNetworkError(err);
    },
});
```

### Shared policy across handlers

```ts
import { createRetry } from '@mcp-utils/retry';

const resilient = createRetry({ attempts: 3, backoff: 'exponential' });

const getUser = resilient(async (ctx, args) => success(await api.users.get(args['id'])));
const getOrg  = resilient(async (ctx, args) => success(await api.orgs.get(args['id'])));
```

### Combine with timeout

```ts
import { withTimeout } from '@mcp-utils/timeout';

// Each attempt gets 5 seconds, up to 3 attempts
const handler = withRetry(
    withTimeout(
        async (ctx, args, _p, signal) => success(await api.call(args, signal)),
        5_000,
    ),
    { attempts: 3 },
);
```

## API

### `withRetry(handler, options)`

| Option | Type | Default | Description |
|---|---|---|---|
| `attempts` | `number` | `3` | Total attempts including the first |
| `backoff` | `'exponential' \| 'linear' \| 'none'` | `'exponential'` | Delay strategy between attempts |
| `baseDelayMs` | `number` | `300` | Base delay in milliseconds |
| `maxDelayMs` | `number` | `10_000` | Maximum delay cap |
| `retryOn` | `(err, attempt) => boolean` | always retry | Predicate controlling whether to retry |

- Only **thrown exceptions** trigger a retry — `isError: true` domain errors are returned as-is
- Stops between attempts if the request has been cancelled via `AbortSignal`
- Returns a structured `toolError` with full diagnostic context after all attempts fail

### `createRetry(options)`

Returns a decorator: `(handler) => wrappedHandler`. Same options as `withRetry`.

---

Part of **[@mcp-utils](https://npmjs.com/org/mcp-utils)** — production-grade utilities for MCP server development.

## License

Apache-2.0