Documentation Index
Fetch the complete documentation index at: https://docs.agentfront.dev/llms.txt
Use this file to discover all available pages before exploring further.
MCP tools often make HTTP requests to external APIs. The HTTP mocking system intercepts fetch() calls, enabling fully offline testing of your MCP server.
Why HTTP Mocking?
Consider a tool that fetches weather data:
@Tool({ name: 'fetch-weather' })
async getWeather(ctx: ToolContext<{ city: string }>) {
// This fetch call needs to be mocked for offline testing
const response = await fetch(`https://api.weather.com/${ctx.input.city}`);
const data = await response.json();
return ctx.text(`Weather: ${data.temperature}°F`);
}
Without HTTP mocking:
- Tests fail offline
- Tests depend on external API availability
- Tests may hit rate limits
- Tests are slow due to network latency
- Tests can’t simulate error conditions
Basic Usage
import { httpMock, httpResponse } from '@frontmcp/testing';
test('mock external API', async ({ mcp }) => {
// Create an HTTP interceptor
const interceptor = httpMock.interceptor();
// Mock a GET request
interceptor.get('https://api.weather.com/london', {
body: { temperature: 72, conditions: 'sunny' },
});
// Call the tool - HTTP request is intercepted
const result = await mcp.tools.call('fetch-weather', { city: 'london' });
expect(result).toBeSuccessful();
expect(result.text()).toContain('72');
// Verify mock was used
interceptor.assertDone();
// Clean up (important!)
interceptor.restore();
});
URL Matching
Exact URL Match
interceptor.get('https://api.example.com/users/1', { body: { id: 1 } });
Partial URL Match
URLs are matched if they contain the specified string:
// Matches any URL containing '/users'
interceptor.get('/users', { body: [{ id: 1 }, { id: 2 }] });
Regular Expression
// Match URLs with pattern
interceptor.get(/api\.example\.com\/users\/\d+/, { body: { id: 1 } });
// Match any user ID
interceptor.get(/\/users\/[a-z0-9-]+/, { body: { id: 'matched' } });
Custom Matcher Function
interceptor.mock({
match: {
url: (url) => url.startsWith('https://api.') && url.includes('/v2/'),
},
response: { body: { version: 2 } },
});
HTTP Methods
Convenience Methods
interceptor.get('/api/data', { body: { result: 'get' } });
interceptor.post('/api/data', { body: { result: 'post' } });
interceptor.put('/api/data', { body: { result: 'put' } });
interceptor.delete('/api/data', { body: { result: 'delete' } });
// Match any method
interceptor.any('/api/data', { body: { result: 'any' } });
Full Mock Definition
interceptor.mock({
match: {
url: '/api/users',
method: 'POST', // Single method
// method: ['POST', 'PUT'], // Multiple methods
},
response: { status: 201, body: { id: 1 } },
});
Request Matching
interceptor.mock({
match: {
url: '/api/protected',
headers: {
'authorization': /^Bearer /,
'content-type': 'application/json',
},
},
response: { body: { authenticated: true } },
});
Match by Request Body
// Exact match
interceptor.mock({
match: {
url: '/api/users',
method: 'POST',
body: { name: 'John', email: 'john@example.com' },
},
response: { status: 201, body: { id: 1 } },
});
// Partial match - only checks specified keys
interceptor.mock({
match: {
url: '/api/users',
method: 'POST',
body: { name: 'John' }, // Matches if body contains name: 'John'
},
response: { status: 201, body: { id: 1 } },
});
// Custom matcher
interceptor.mock({
match: {
url: '/api/users',
body: (body) => body.age >= 18,
},
response: { body: { eligible: true } },
});
Response Helpers
httpResponse Utilities
import { httpResponse } from '@frontmcp/testing';
// JSON response (auto-sets Content-Type)
interceptor.get('/api/data', httpResponse.json({ id: 1 }));
// Text response
interceptor.get('/api/text', httpResponse.text('Hello World'));
// HTML response
interceptor.get('/page', httpResponse.html('<h1>Welcome</h1>'));
// Error responses
interceptor.get('/api/missing', httpResponse.notFound());
interceptor.get('/api/auth', httpResponse.unauthorized());
interceptor.get('/api/forbidden', httpResponse.forbidden());
interceptor.get('/api/error', httpResponse.serverError('Database down'));
// Custom error
interceptor.get('/api/rate-limit', httpResponse.error(429, 'Too many requests'));
// Delayed response
interceptor.get('/api/slow', httpResponse.delayed({ data: 'result' }, 500));
Full Response Object
interceptor.mock({
match: { url: '/api/custom' },
response: {
status: 201,
statusText: 'Created',
headers: {
'x-request-id': 'abc123',
'cache-control': 'no-cache',
},
body: { id: 1, created: true },
delay: 100, // ms
},
});
One-Time Mocks
Create mocks that only match a limited number of times:
test('simulate retry scenario', async ({ mcp }) => {
const interceptor = httpMock.interceptor();
// First request fails
interceptor.mock({
match: { url: '/api/flaky' },
response: httpResponse.serverError('Temporary failure'),
times: 1,
});
// Subsequent requests succeed
interceptor.mock({
match: { url: '/api/flaky' },
response: { body: { success: true } },
});
// Tool with retry logic
const result = await mcp.tools.call('resilient-fetch', {});
expect(result).toBeSuccessful();
interceptor.restore();
});
Call Tracking
Track Mock Usage
test('verify HTTP calls', async ({ mcp }) => {
const interceptor = httpMock.interceptor();
const handle = interceptor.post('/api/users', { body: { id: 1 } });
await mcp.tools.call('create-user', { name: 'John', email: 'john@test.com' });
// Check call count
expect(handle.callCount()).toBe(1);
// Get call details
const calls = handle.calls();
expect(calls[0].url).toContain('/api/users');
expect(calls[0].method).toBe('POST');
expect(calls[0].headers['content-type']).toBe('application/json');
expect(calls[0].body).toEqual({ name: 'John', email: 'john@test.com' });
interceptor.restore();
});
Wait for Calls
test('wait for async operations', async ({ mcp }) => {
const interceptor = httpMock.interceptor();
const handle = interceptor.get('/api/webhook', { body: { received: true } });
// Trigger async operation that makes HTTP call
await mcp.tools.call('trigger-webhook', {});
// Wait for up to 5 seconds for 3 calls
const calls = await handle.waitForCalls(3, 5000);
expect(calls).toHaveLength(3);
interceptor.restore();
});
Passthrough Mode
Allow unmatched requests to reach the real network:
test('partial mocking', async ({ mcp }) => {
const interceptor = httpMock.interceptor();
// Only mock specific endpoint
interceptor.get('/api/mocked', { body: { mocked: true } });
// Allow other requests through
interceptor.allowPassthrough(true);
// This uses the mock
const result1 = await fetch('/api/mocked');
expect(await result1.json()).toEqual({ mocked: true });
// This hits the real API
const result2 = await fetch('https://real-api.com/data');
// ... real response
interceptor.restore();
});
By default, unmatched requests throw an error. Only enable passthrough when you specifically need some requests to hit real endpoints.
Verification
Assert All Mocks Used
test('verify all mocks used', async ({ mcp }) => {
const interceptor = httpMock.interceptor();
interceptor.get('/api/users', { body: [] }, { times: 1 });
interceptor.get('/api/orders', { body: [] }, { times: 1 });
await mcp.tools.call('fetch-all-data', {});
// Throws if any mock with `times` wasn't used
interceptor.assertDone();
interceptor.restore();
});
Check Pending Mocks
test('check unused mocks', async ({ mcp }) => {
const interceptor = httpMock.interceptor();
interceptor.get('/api/users', { body: [] }, { times: 1 });
interceptor.get('/api/orders', { body: [] }, { times: 1 });
await mcp.tools.call('fetch-users-only', {});
// Check which mocks weren't used
const pending = interceptor.pending();
expect(pending).toHaveLength(1);
expect(pending[0].match.url).toBe('/api/orders');
interceptor.restore();
});
Cleanup
Always call interceptor.restore() to restore the original fetch function. Failing to do so will affect other tests.
Pattern: Using try/finally
test('with cleanup', async ({ mcp }) => {
const interceptor = httpMock.interceptor();
try {
interceptor.get('/api/data', { body: { result: true } });
await mcp.tools.call('fetch-data', {});
} finally {
interceptor.restore();
}
});
Pattern: beforeEach/afterEach
let interceptor: ReturnType<typeof httpMock.interceptor>;
beforeEach(() => {
interceptor = httpMock.interceptor();
});
afterEach(() => {
interceptor.restore();
});
test('test 1', async ({ mcp }) => {
interceptor.get('/api/data', { body: { test: 1 } });
// ...
});
test('test 2', async ({ mcp }) => {
interceptor.get('/api/data', { body: { test: 2 } });
// ...
});
Global Disable
afterAll(() => {
// Disable all HTTP mocking globally
httpMock.disable();
});
Real-World Examples
Testing OpenAPI Adapter
test('openapi adapter calls external API', async ({ mcp }) => {
const interceptor = httpMock.interceptor();
interceptor.get('https://api.example.com/openapi.json', {
body: {
openapi: '3.0.0',
paths: {
'/users': {
get: { operationId: 'getUsers' },
},
},
},
});
interceptor.get('https://api.example.com/users', {
body: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
],
});
const result = await mcp.tools.call('openapi:getUsers', {});
expect(result).toBeSuccessful();
expect(result.json()).toHaveLength(2);
interceptor.restore();
});
Testing Error Handling
test('handles API errors gracefully', async ({ mcp }) => {
const interceptor = httpMock.interceptor();
interceptor.get('/api/data', httpResponse.serverError('Database unavailable'));
const result = await mcp.tools.call('fetch-with-fallback', {});
// Tool should handle error and return fallback
expect(result).toBeSuccessful();
expect(result.json()).toEqual({ fallback: true });
interceptor.restore();
});
Testing Rate Limiting
test('respects rate limits', async ({ mcp }) => {
const interceptor = httpMock.interceptor();
// First request succeeds
interceptor.mock({
match: { url: '/api/data' },
response: { body: { data: 'first' } },
times: 1,
});
// Second request rate limited
interceptor.mock({
match: { url: '/api/data' },
response: httpResponse.error(429, 'Rate limit exceeded'),
});
const result1 = await mcp.tools.call('fetch-data', {});
expect(result1).toBeSuccessful();
const result2 = await mcp.tools.call('fetch-data', {});
expect(result2).toBeError();
interceptor.restore();
});
Best Practices
Do:
- Always call
restore() in cleanup
- Use
httpResponse helpers for common responses
- Verify mocks were used with
assertDone()
- Mock at the appropriate level of detail
Don’t:
- Forget cleanup (breaks other tests)
- Enable passthrough without good reason
- Over-mock (test real HTTP handling sometimes)
- Hard-code response data that should be dynamic