@buildwithabid/mcp-testkit
v1.0.0
Published
Testing framework for MCP (Model Context Protocol) servers. Custom matchers, schema validation, fuzz testing, and snapshot support for vitest and jest.
Maintainers
Readme
mcp-testkit
The Testing Framework for MCP Servers
Test your Model Context Protocol tools, resources, and prompts with expressive matchers, schema validation, fuzz testing, and snapshot support.
Getting Started · Features · API Reference · Contributing
Why mcp-testkit?
Building MCP servers is straightforward. Knowing they work correctly is the hard part.
| Problem | How mcp-testkit solves it |
|:--------|:-------------------------|
| No standard testing patterns | One-line harness setup for any MCP server |
| Boilerplate transport wiring | createHarness() handles InMemoryTransport or stdio for you |
| Manual result.content[0].text checks | Purpose-built matchers like toBeSuccessful() and toHaveTextContent() |
| No schema validation | validateToolSchema() catches issues before production |
| Edge cases found by users, not tests | Built-in fuzz testing with reproducible seeds |
Write tests for MCP servers the same way you write tests for REST APIs — fast, expressive, and reliable.
Table of Contents
Getting Started
Installation
Not published to a registry yet. Install from source with the steps below; the commands in this README assume you have done so.
git clone https://github.com/BuildWithAbid/mcp-testkit.git
cd mcp-testkit
npm install && npm run build
npm link # then `npm link mcp-testkit` in your projectPeer dependencies (you likely already have these):
npm install @modelcontextprotocol/sdk zodYour First Test
import { describe, it, expect } from "vitest";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { createHarness } from "mcp-testkit";
import { mcpMatchers } from "mcp-testkit/matchers";
expect.extend(mcpMatchers);
const server = new McpServer({ name: "my-server", version: "1.0.0" });
server.tool(
"greet",
"Greet a user",
{ name: z.string() },
async ({ name }) => ({
content: [{ type: "text", text: `Hello, ${name}!` }],
})
);
describe("My MCP Server", () => {
it("greets users", async () => {
const harness = await createHarness(server);
const tools = await harness.listTools();
expect(tools).toHaveToolNamed("greet");
const result = await harness.callTool("greet", { name: "World" });
expect(result).toBeSuccessful();
expect(result).toHaveTextContent("Hello, World!");
await harness.close();
});
});Auto-Setup (Optional)
Skip the expect.extend() boilerplate by registering matchers automatically:
// vitest.config.ts
export default {
test: {
setupFiles: ["mcp-testkit/setup/vitest"],
},
};Features
Test Harness
Create a fully wired MCP client-server pair in one line. No transport setup, no boilerplate.
import { createHarness } from "mcp-testkit";
// In-memory harness (unit tests) — pass an McpServer instance
const harness = await createHarness(server);
// Stdio harness (integration tests) — pass a command config
const harness = await createHarness({
command: "node",
args: ["./dist/server.js"],
env: { API_KEY: "test" },
});
// Same API for both modes
const tools = await harness.listTools();
const result = await harness.callTool("my-tool", { input: "test" });
const resources = await harness.listResources();
const prompts = await harness.listPrompts();
await harness.close();| | In-memory | Stdio |
|---|---|---|
| Speed | Instant | Spawns a child process |
| Use case | Unit tests, CI | Integration tests, E2E |
| Setup | Pass McpServer instance | Pass { command, args } config |
| Isolation | Shares process memory | Full process isolation |
Use in-memory for fast iteration during development. Use stdio to test the actual binary your users will run.
Custom Matchers
Expressive assertions purpose-built for MCP testing. Works with Vitest and Jest.
Tool Discovery
expect(tools).toHaveToolNamed("search");
expect(tools).toHaveToolCount(5);
expect(tools).toHaveToolDescription("search", /find items/i);
expect(tools).toHaveInputSchema("search", { query: {}, limit: {} });Tool Results
expect(result).toBeSuccessful();
expect(result).toBeToolError();
expect(result).toHaveTextContent("expected output");
expect(result).toHaveTextContent(/pattern/);
expect(result).toHaveContentCount(1);
expect(result).toHaveContentType("text");Schema Validation
Validate that your tool schemas follow MCP best practices before they reach production.
import { validateToolSchema, validateAllToolSchemas } from "mcp-testkit";
const tools = await harness.listTools();
// Validate a single tool
const issues = validateToolSchema(tools[0]);
// [{ path: "properties.query", message: "Missing description", severity: "warning" }]
// Validate all tools at once
const allIssues = validateAllToolSchemas(tools);
// Map<string, SchemaIssue[]>Input Generation
Automatically generate valid and edge-case inputs from any tool's JSON Schema.
import { generateValidInput, generateEdgeCaseInputs } from "mcp-testkit";
const schema = tools[0].inputSchema;
// Generate a minimal valid input
const input = generateValidInput(schema);
// { query: "test" }
// Generate edge cases for thorough testing
const edgeCases = generateEdgeCaseInputs(schema);
// [
// { label: "empty object", input: {} },
// { label: "missing required: query", input: {} },
// { label: "wrong type for: query", input: { query: 42 } },
// { label: "boundary: empty string for query", input: { query: "" } },
// { label: "boundary: very long string for query", input: { query: "a".repeat(10000) } },
// ]Fuzz Testing
Throw random and adversarial inputs at your tools to surface crashes before your users do.
import { fuzzTool, fuzzAllTools } from "mcp-testkit";
// Fuzz a single tool — with a seed for reproducibility
const result = await fuzzTool(harness, "search", {
runs: 100,
seed: 42,
assert: (toolResult) => {
expect(toolResult.content.length).toBeGreaterThan(0);
},
});
console.log(`${result.passed}/${result.runs} passed`);
console.log("Failures:", result.errors);
// Fuzz every tool on the server
const results = await fuzzAllTools(harness, { runs: 50, seed: 42 });Snapshot Testing
Custom serializer that strips non-deterministic fields for stable, readable snapshots.
import { toolResultSerializer } from "mcp-testkit";
expect.addSnapshotSerializer(toolResultSerializer);
const result = await harness.callTool("greet", { name: "World" });
expect(result).toMatchSnapshot();Text Helpers
Utility functions for extracting and checking text content in tool results.
import { getTexts, getFirstText, hasText } from "mcp-testkit";
const result = await harness.callTool("greet", { name: "World" });
getTexts(result); // ["Hello, World!"]
getFirstText(result); // "Hello, World!"
hasText(result, /hello/i); // trueAPI Reference
Harness
| Function | Description |
|:---------|:------------|
| createHarness(server) | Create an in-memory test harness from an McpServer instance |
| createHarness(config) | Create a stdio test harness from { command, args?, env?, cwd? } |
| Method | Returns | Description |
|:-------|:--------|:------------|
| harness.listTools() | ToolInfo[] | List all registered tools |
| harness.callTool(name, args?) | ToolResult | Call a tool by name |
| harness.listResources() | ResourceInfo[] | List all resources |
| harness.readResource(uri) | ResourceResult | Read a resource by URI |
| harness.listPrompts() | PromptInfo[] | List all prompts |
| harness.getPrompt(name, args?) | PromptResult | Get a prompt by name |
| harness.getServerCapabilities() | Record<string, unknown> | Get server capabilities |
| harness.close() | void | Close connection and clean up (idempotent) |
Matchers
| Matcher | Applies to | Description |
|:--------|:-----------|:------------|
| toHaveToolNamed(name) | ToolInfo[] | Assert a tool with the given name exists |
| toHaveToolCount(n) | ToolInfo[] | Assert the exact number of tools |
| toHaveToolDescription(name, expected) | ToolInfo[] | Assert a tool's description matches a string or regex |
| toHaveInputSchema(name, props) | ToolInfo[] | Assert a tool's schema contains expected properties |
| toBeSuccessful() | ToolResult | Assert the result has no error flag |
| toBeToolError() | ToolResult | Assert the result has the error flag |
| toHaveTextContent(expected) | ToolResult | Assert text content matches a string or regex |
| toHaveContentCount(n) | ToolResult | Assert the exact number of content items |
| toHaveContentType(type) | ToolResult | Assert at least one content item has the given type |
Schema & Validation
| Function | Description |
|:---------|:------------|
| validateToolSchema(tool) | Validate a single tool's JSON Schema, returns SchemaIssue[] |
| validateAllToolSchemas(tools) | Validate all tools, returns Map<string, SchemaIssue[]> |
| validateOutput(output, schema) | Validate output data against a JSON Schema |
| generateValidInput(schema) | Generate a minimal valid input object from a schema |
| generateEdgeCaseInputs(schema) | Generate an array of edge-case inputs for boundary testing |
Fuzz
| Function | Description |
|:---------|:------------|
| fuzzTool(harness, name, options?) | Fuzz test a single tool with random inputs |
| fuzzAllTools(harness, options?) | Fuzz test every tool on the server |
FuzzOptions
| Option | Type | Default | Description |
|:-------|:-----|:--------|:------------|
| runs | number | 50 | Number of random inputs to generate |
| seed | number | Date.now() | Seed for reproducible runs |
| assert | (result, input) => void | — | Custom assertion applied to every result |
Snapshot
| Export | Description |
|:-------|:------------|
| toolResultSerializer | Pre-configured snapshot serializer (strips _meta, _timestamp, etc.) |
| createToolResultSerializer(options?) | Create a custom serializer with your own stripFields |
Helpers
| Function | Description |
|:---------|:------------|
| getTexts(result) | Extract all text strings from a ToolResult |
| getFirstText(result) | Extract the first text string, or undefined |
| hasText(result, pattern) | Check if any text content matches a string or regex |
Compatibility
| Dependency | Supported Versions | |:-----------|:-------------------| | Node.js | 18, 20, 22+ | | Test Runners | Vitest, Jest | | MCP SDK | @modelcontextprotocol/sdk v1.12+ | | Zod | Zod v3.23+ or v4 | | TypeScript | 5.x |
Contributing
Contributions are welcome! Please open an issue first to discuss what you'd like to change.
git clone https://github.com/BuildWithAbid/mcp-testkit.git
cd mcp-testkit
npm install
npm test
npm run build