@mcp-utils/testing
v1.0.0
Published
Testing utilities for MCP servers: mock handlers, fake contexts, fixture builders — powered by vurb.
Maintainers
Readme
@mcp-utils/testing
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.
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
npm install --save-dev @mcp-utils/testingUsage
Test your actual handler
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
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
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
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
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 — production-grade utilities for MCP server development.
License
Apache-2.0
