npm package: @mcp-utils/testing
Description: Testing utilities for MCP servers: mock handlers, fake contexts, fixture builders — powered by vurb.
Latest version: 1.0.0

# @mcp-utils/testing

<div align="center">

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

</div>

---

MCP tool handlers are just async functions. They take args, call some logic, return a result.

So why does testing them feel like you need a running server, a mock SDK, a fake AI client, and three environment variables just to assert that a function returned the right shape?

You don't. **A handler is a function. Test it like one.**

```ts
import { callHandler, mockTool } from '@mcp-utils/testing';

// Any handler. Any args. In any test file. No server required.
const result = await callHandler(myHandler, { userId: 'u_123' });
expect(result.isError).toBeFalsy();
```

## Install

```bash
npm install --save-dev @mcp-utils/testing
```

## Usage

### Test your actual handler

```ts
import { callHandler, createFakeContext } from '@mcp-utils/testing';
import { getUserHandler } from './handlers/getUser.js';

it('returns the user profile', async () => {
    const ctx = createFakeContext<AppContext>({ userId: 'u_123', role: 'admin' });
    const result = await callHandler(getUserHandler, { targetId: 'u_456' }, ctx);
    expect(result.isError).toBeFalsy();
});
```

### Test error handling

```ts
import { mockToolError, callHandler } from '@mcp-utils/testing';

it('surfaces errors correctly', async () => {
    const result = await callHandler(mockToolError('Service unavailable'));
    expect(result.isError).toBe(true);
});
```

### Test your retry wrapper

```ts
import { mockThrow, callHandler } from '@mcp-utils/testing';
import { withRetry } from '@mcp-utils/retry';

it('retries on failure and eventually succeeds', async () => {
    let calls = 0;
    const flaky = async (ctx, args) => {
        if (++calls < 3) throw new Error('Not ready yet');
        return mockTool('recovered')(ctx, args);
    };

    const result = await callHandler(
        withRetry(flaky, { attempts: 3, backoff: 'none' }),
    );
    expect(result.isError).toBeFalsy();
    expect(calls).toBe(3);
});
```

### Test your timeout wrapper

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

it('times out and returns an error', async () => {
    const slow = mockDelay(10_000, { value: 'too slow' });
    const bounded = withTimeout(slow, 50);

    const result = await callHandler(bounded);
    expect(result.isError).toBe(true);
});
```

### Test AbortSignal cancellation

```ts
import { mockDelay, callHandler } from '@mcp-utils/testing';

it('stops when the request is cancelled', async () => {
    const controller = new AbortController();
    setTimeout(() => controller.abort(), 20);

    await expect(
        callHandler(mockDelay(10_000, {}), {}, undefined, controller.signal)
    ).rejects.toBeDefined();
});
```

## API

### Mock factories

| Function | Description |
|---|---|
| `mockTool(fixture)` | Returns a handler that always succeeds with `fixture` |
| `mockToolError(message, code?)` | Returns a handler that always returns a tool error |
| `mockThrow(error?)` | Returns a handler that always throws — for testing retry and error-boundary wrappers |
| `mockDelay(ms, fixture?)` | Returns a handler that waits `ms` ms — for testing timeout and cancellation logic |

### Context and execution

| Function | Description |
|---|---|
| `createFakeContext<T>(overrides?)` | Build a typed fake context — no boilerplate, no required constructor |
| `callHandler(handler, args?, ctx?, signal?)` | Call any handler directly with sensible defaults for optional params |

---

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

## License

Apache-2.0