MP-301b · Module 1
Mock Transports & Test Doubles
3 min read
The MCP SDK provides InMemoryTransport for testing the full protocol cycle without stdio or HTTP. You create a linked pair of transports — one for the client, one for the server — connect them, and run your tests through the client. This tests everything: schema validation, request routing, handler execution, error formatting, and response serialization. It is the closest you can get to a real MCP conversation without spawning processes or opening ports.
Beyond InMemoryTransport, you need test doubles for your server's external dependencies. A fixture server is a pre-configured MCP server with deterministic responses — every tool call returns the same result for the same input, regardless of database state, API availability, or network conditions. Build your fixture server by replacing dependency injections with in-memory implementations: a Map instead of a database, static JSON instead of API calls. This makes your integration tests reproducible and CI-friendly.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createServer } from "../../src/server.js";
// Fixture data — deterministic, no external dependencies
const fixtureDb = new Map([
["CUS-001", { id: "CUS-001", name: "Acme Corp", status: "active" }],
["CUS-002", { id: "CUS-002", name: "Globex Inc", status: "churned" }],
]);
export async function createTestPair() {
// Server with fixture dependencies
const server = createServer({
crm: {
findCustomer: async (id: string) => fixtureDb.get(id) ?? null,
listCustomers: async () => [...fixtureDb.values()],
},
});
const client = new Client(
{ name: "test-client", version: "1.0.0" },
{ capabilities: {} },
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
return {
client,
server,
cleanup: async () => {
await client.close();
await server.close();
},
};
}
// Usage in tests:
// const { client, cleanup } = await createTestPair();
// const result = await client.callTool({ name: "get_customer", arguments: { customer_id: "CUS-001" } });
// await cleanup();
- Build a server factory Create a `createServer(deps)` function that accepts injected dependencies. Production passes real clients; tests pass fixtures. This is the foundation of testability.
- Create fixture data Build a static dataset that covers your test cases: valid records, edge cases, and expected-not-found IDs. Store it in a test helpers module.
- Wire up InMemoryTransport Create a `createTestPair()` helper that returns a connected client + cleanup function. Use it in every integration test to avoid transport boilerplate.