npm package: @mcp-utils/timeout
Description: Timeout wrapper for MCP tool handlers with AbortSignal propagation — powered by vurb.
Latest version: 1.0.0

# @mcp-utils/timeout

<div align="center">

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

</div>

---

Your MCP tool calls an external API. The API stops responding. No error. No response. Just silence.

The LLM waits. The user waits. The connection stays open, burning resources, until something upstream finally gives up — if it ever does.

**MCP has no built-in timeout.** Every tool you build is vulnerable unless you add one yourself.

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

// Before: one hung API = your agent stalls indefinitely
handler: async (ctx, args) => success(await slowApi.call(args))

// After: fails fast — clear error, clean cancellation, agent moves on
handler: withTimeout(
    async (ctx, args, _p, signal) => success(await slowApi.call(args, { signal })),
    10_000
)
```

## Install

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

## Usage

### Basic timeout

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

const generateReport = withTimeout(
    async (ctx, args) => success(await reports.generate(args)),
    15_000, // fail after 15 seconds
);
```

### Propagate the signal (important)

Pass the signal to downstream calls so they cancel cleanly when the timeout fires — no orphaned DB queries, no leaking HTTP connections:

```ts
const fetchData = withTimeout(
    async (ctx, args, _progress, signal) => {
        const rows = await db.query({ where: args, signal });   // cancels on timeout
        const meta = await fetch(apiUrl, { signal });           // cancels on timeout
        return success({ rows, meta });
    },
    8_000,
);
```

### Apply a consistent policy across tools

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

const bounded = createTimeout(5_000);

const getUser    = bounded(async (ctx, args, _p, signal) => success(await db.users.get(args['id'], signal)));
const getProduct = bounded(async (ctx, args, _p, signal) => success(await db.products.get(args['id'], signal)));
```

### Stack with retry

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

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

## API

### `withTimeout(handler, ms)`

- Injects an `AbortSignal` into the handler for clean downstream cancellation
- Chains with existing upstream signals from the MCP request — if the client cancels, the tool cancels too
- Returns `toolError('TIMEOUT')` on expiry with a clear message
- Returns an error immediately if the upstream signal was already aborted before the tool was called

### `createTimeout(ms)`

Returns a decorator: `(handler) => wrappedHandler`.

---

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

## License

Apache-2.0