Testing
Prerequisites
Install MCP Fusion before following this recipe: npm install @vinkius-core/mcp-fusion @modelcontextprotocol/sdk zod — or scaffold a project with npx fusion create.
- Introduction
- FusionTester Setup
- Executing Tools
- Firewall Tests — Field Whitelist
- Rules Verification
- Middleware & Guards
- Generator Tests
Introduction
MCP Fusion ships @vinkius-core/mcp-fusion-testing — a dedicated testing harness that enables Automated AI Tool Testing. It lets you execute tools, inspect responses, verify Presenter rules, and assert on field whitelists without spinning up a full MCP server.
The philosophy: test perception, not plumbing. Instead of testing "does findMany return rows?", test "does the AI receive exactly the fields it should, with the right rules attached?" This focuses your testing on guaranteeing Deterministic LLM Output and ensuring absolute Data Exfiltration Prevention before your agents ever reach production.
FusionTester Setup
Create a shared tester instance in your test setup file:
// tests/setup.ts
import { FusionTester } from '@vinkius-core/mcp-fusion-testing';
import { registry } from '../src/index.js';
export function createTester(contextOverrides?: Partial<AppContext>) {
return new FusionTester(registry, {
db: createTestDatabase(),
tenantId: 'test-tenant',
userId: 'test-user',
...contextOverrides,
});
}FusionTester wraps your registry with a test-friendly API. It executes tools with the same middleware chain, Presenter pipeline, and response builder as production — but without the MCP transport layer.
Executing Tools
import { describe, it, expect } from 'vitest';
import { createTester } from './setup.js';
describe('projects.list', () => {
it('returns projects for the current tenant', async () => {
const tester = createTester();
const result = await tester.callTool('projects.list', {
status: 'active',
});
expect(result.isError).toBe(false);
expect(result.content).toBeDefined();
expect(result.content[0].text).toContain('active');
});
it('returns error for invalid parameters', async () => {
const tester = createTester();
const result = await tester.callTool('projects.list', {
status: 'invalid_status', // not in enum
});
expect(result.isError).toBe(true);
});
});callTool(name, args) executes the full pipeline: validation → middleware → handler → Presenter → response. The result is an MCP ToolResponse.
Firewall Tests — Field Whitelist
The most important test category: verify that internal fields never leak to the AI. The Presenter's Zod .strict() schema strips undeclared fields — but you should test it:
// tests/firewall/invoices.firewall.test.ts
import { describe, it, expect } from 'vitest';
import { createTester } from '../setup.js';
describe('Invoice firewall', () => {
it('strips internal fields from response', async () => {
const tester = createTester();
const result = await tester.callTool('billing.get_invoice', { id: 'INV-1' });
const data = JSON.parse(result.content[0].text);
// These fields MUST be present
expect(data).toHaveProperty('id');
expect(data).toHaveProperty('amount_cents');
expect(data).toHaveProperty('status');
// These MUST NOT leak
expect(data).not.toHaveProperty('stripe_customer_id');
expect(data).not.toHaveProperty('internal_notes');
expect(data).not.toHaveProperty('password_hash');
});
});IMPORTANT
Firewall tests are your security boundary. Run them on every CI push. A failing firewall test means sensitive data could reach the AI.
Rules Verification
Verify that system rules appear in the response when (and only when) they should:
// tests/rules/invoices.rules.test.ts
describe('Invoice rules', () => {
it('includes currency rules in response', async () => {
const tester = createTester();
const result = await tester.callTool('billing.get_invoice', { id: 'INV-1' });
const text = result.content.map(c => c.text).join('\n');
expect(text).toContain('CENTS');
expect(text).toContain('Divide by 100');
});
it('includes RBAC restriction for non-admins', async () => {
const tester = createTester({ user: { role: 'viewer' } });
const result = await tester.callTool('employees.get', { id: 'EMP-1' });
const text = result.content.map(c => c.text).join('\n');
expect(text).toContain('RESTRICTED');
expect(text).toContain('Do NOT display salary');
});
});Middleware & Guards
Test that middleware blocks unauthorized access:
// tests/guards/auth.guard.test.ts
describe('Auth middleware', () => {
it('rejects unauthenticated requests', async () => {
const tester = createTester({ token: '' });
const result = await tester.callTool('users.list', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Authentication required');
});
it('rejects non-admin from admin endpoints', async () => {
const tester = createTester({ token: memberToken });
const result = await tester.callTool('users.delete', { user_id: 'U-1' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('admin role required');
});
});Generator Tests
Test streaming handlers by collecting progress events:
describe('Streaming', () => {
it('emits progress events', async () => {
const tester = createTester();
const progressEvents: { progress: number; message: string }[] = [];
const result = await tester.callTool(
'repo.analyze',
{ url: 'https://github.com/test/repo' },
{ onProgress: (p) => progressEvents.push(p) },
);
expect(result.isError).toBe(false);
expect(progressEvents.length).toBeGreaterThan(0);
expect(progressEvents[progressEvents.length - 1].progress).toBe(100);
});
});The onProgress callback collects every yield progress() from the generator handler.