
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
@crashbytes/mcp-test-kit
Advanced tools
Testing framework for Model Context Protocol (MCP) servers - Jest/Vitest for MCP
A comprehensive testing framework for Model Context Protocol (MCP) servers
Model Context Protocol (MCP) is an open protocol developed by Anthropic that enables AI assistants like Claude to securely interact with external tools, data sources, and services. Think of it as a standardized way for AI models to:
Here are some practical MCP servers you might build:
graph LR
A[Claude<br/>Client] <-->|MCP| B[MCP Server<br/>Your Code]
B <--> C[Your Data<br/>or Service]
The MCP server acts as a bridge between Claude and your data/services, exposing them through a standardized protocol.
MCP Test Kit is a testing framework that makes it easy to write automated tests for your MCP servers. It provides:
Without MCP Test Kit, testing an MCP server is challenging:
With MCP Test Kit:
toBeValidMCPTool() and toMatchMCPToolResponse()If you're building an MCP server, MCP Test Kit helps you:
Let's say you built a weather MCP server. With MCP Test Kit, you can write tests like:
it('should return weather data for San Francisco', async () => {
const result = await client.callTool('get-weather', {
city: 'San Francisco'
});
expect(result).toMatchMCPToolResponse();
expect(result.content[0].text).toContain('temperature');
expect(result.content[0].text).toContain('San Francisco');
});
it('should handle invalid city names', async () => {
await expect(
client.callTool('get-weather', { city: '' })
).rejects.toMatchMCPError({
code: -32602,
message: /invalid.*city/i
});
});
# Using npm
npm install --save-dev @crashbytes/mcp-test-kit
# Using yarn
yarn add -D @crashbytes/mcp-test-kit
# Using pnpm
pnpm add -D @crashbytes/mcp-test-kit
// tests/weather-server.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createMCPTestClient } from '@crashbytes/mcp-test-kit';
import type { MCPTestClient } from '@crashbytes/mcp-test-kit';
import '@crashbytes/mcp-test-kit/matchers';
describe('Weather MCP Server', () => {
let client: MCPTestClient;
beforeAll(async () => {
// Connect to your MCP server
client = await createMCPTestClient({
command: 'node',
args: ['dist/server.js'],
});
});
afterAll(async () => {
await client.disconnect();
});
it('should list available tools', async () => {
const tools = await client.listTools();
expect(tools).toHaveLength(1);
expect(tools[0]).toBeValidMCPTool();
expect(tools[0].name).toBe('get-weather');
});
it('should get weather for a city', async () => {
const result = await client.callTool('get-weather', {
city: 'London',
});
expect(result).toMatchMCPToolResponse();
expect(result.content[0].text).toContain('London');
});
});
npm test
That's it! You're now testing your MCP server.
The MCPTestClient is your main interface for testing MCP servers. It:
Key Methods:
listTools(): Get all available tools from the servercallTool(name, args): Execute a tool with argumentslistResources(): Get all available resourcesreadResource(uri): Read a specific resourcelistPrompts(): Get all available promptsgetPrompt(name, args): Get a specific promptMCP Test Kit provides custom Vitest matchers for MCP-specific assertions:
toBeValidMCPTool()Validates that an object is a properly formatted MCP tool:
const tool = {
name: 'calculate',
description: 'Performs calculations',
inputSchema: {
type: 'object',
properties: {
expression: { type: 'string' }
}
}
};
expect(tool).toBeValidMCPTool();
toBeValidMCPResource()Validates that an object is a properly formatted MCP resource:
const resource = {
uri: 'file:///data/users.json',
name: 'Users Database',
description: 'List of all users'
};
expect(resource).toBeValidMCPResource();
toMatchMCPToolResponse()Validates the structure of a tool response:
const response = await client.callTool('get-weather', { city: 'Paris' });
expect(response).toMatchMCPToolResponse();
expect(response.content[0].text).toContain('Paris');
toMatchMCPError(error)Validates MCP error responses:
await expect(
client.callTool('invalid-tool', {})
).rejects.toMatchMCPError({
code: -32601, // Method not found
message: /not found/i
});
toHaveMCPProtocolVersion(version)Validates the MCP protocol version:
const info = await client.getServerInfo();
expect(info).toHaveMCPProtocolVersion('2024-11-05');
Creates and connects to an MCP test client.
Parameters:
interface MCPTestClientConfig {
command: string; // Command to run (e.g., 'node', 'python')
args?: string[]; // Arguments for the command
env?: Record<string, string>; // Environment variables
timeout?: number; // Timeout in milliseconds (default: 5000)
transport?: 'stdio'; // Transport type (only stdio supported)
debug?: boolean; // Enable debug logging
}
Returns: Promise<MCPTestClient>
Example:
const client = await createMCPTestClient({
command: 'node',
args: ['dist/server.js'],
env: {
NODE_ENV: 'test',
DATABASE_URL: 'sqlite::memory:'
},
timeout: 10000,
debug: true
});
listTools(): Promise<MCPTool[]>Lists all tools available on the MCP server.
const tools = await client.listTools();
console.log(tools[0].name); // 'get-weather'
callTool(name: string, args?: object): Promise<MCPToolResult>Executes a tool with the given arguments.
const result = await client.callTool('calculate', {
expression: '2 + 2'
});
listResources(): Promise<MCPResource[]>Lists all resources available on the MCP server.
const resources = await client.listResources();
console.log(resources[0].uri); // 'file:///data/users.json'
readResource(uri: string): Promise<MCPResourceContent>Reads the content of a specific resource.
const content = await client.readResource('file:///data/users.json');
console.log(content.text);
listPrompts(): Promise<MCPPrompt[]>Lists all prompts available on the MCP server.
const prompts = await client.listPrompts();
getPrompt(name: string, args?: Record<string, string>): Promise<unknown>Gets a specific prompt with arguments.
const prompt = await client.getPrompt('code-review', {
language: 'typescript'
});
disconnect(): Promise<void>Disconnects from the MCP server and cleans up resources.
await client.disconnect();
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createMCPTestClient, MCPTestClient } from '@crashbytes/mcp-test-kit';
import '@crashbytes/mcp-test-kit/matchers';
describe('Calculator MCP Server', () => {
let client: MCPTestClient;
beforeAll(async () => {
client = await createMCPTestClient({
command: 'node',
args: ['dist/calculator-server.js'],
});
});
afterAll(async () => {
await client.disconnect();
});
describe('Basic Operations', () => {
it('should add two numbers', async () => {
const result = await client.callTool('calculate', {
operation: 'add',
a: 5,
b: 3
});
expect(result).toMatchMCPToolResponse();
expect(result.content[0].text).toBe('8');
});
it('should handle division by zero', async () => {
await expect(
client.callTool('calculate', {
operation: 'divide',
a: 10,
b: 0
})
).rejects.toMatchMCPError({
code: -32602,
message: /division by zero/i
});
});
});
describe('Tool Validation', () => {
it('should have valid tool schema', async () => {
const tools = await client.listTools();
const calcTool = tools.find(t => t.name === 'calculate');
expect(calcTool).toBeValidMCPTool();
expect(calcTool?.inputSchema.properties).toHaveProperty('operation');
expect(calcTool?.inputSchema.properties).toHaveProperty('a');
expect(calcTool?.inputSchema.properties).toHaveProperty('b');
});
});
});
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createMCPTestClient, MCPTestClient } from '@crashbytes/mcp-test-kit';
import '@crashbytes/mcp-test-kit/matchers';
describe('Database MCP Server', () => {
let client: MCPTestClient;
beforeAll(async () => {
client = await createMCPTestClient({
command: 'node',
args: ['dist/db-server.js'],
env: {
DATABASE_URL: 'sqlite::memory:',
NODE_ENV: 'test'
}
});
});
afterAll(async () => {
await client.disconnect();
});
it('should query users from database', async () => {
const result = await client.callTool('query', {
sql: 'SELECT * FROM users WHERE age > 18'
});
expect(result).toMatchMCPToolResponse();
const data = JSON.parse(result.content[0].text);
expect(Array.isArray(data)).toBe(true);
});
it('should list database resources', async () => {
const resources = await client.listResources();
expect(resources.length).toBeGreaterThan(0);
resources.forEach(resource => {
expect(resource).toBeValidMCPResource();
});
});
it('should read table schema', async () => {
const content = await client.readResource('schema://users');
expect(content.text).toContain('id');
expect(content.text).toContain('name');
expect(content.text).toContain('email');
});
});
import { describe, it, expect } from 'vitest';
import { createMockMCPServer, mockTool } from '@crashbytes/mcp-test-kit/mocks';
describe('Mock MCP Server', () => {
it('should create a mock server with tools', async () => {
const server = createMockMCPServer({
tools: [
mockTool(
'greet',
async (args) => ({
content: [{ type: 'text', text: `Hello, ${args.name}!` }]
}),
{
description: 'Greets a person',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string' }
}
}
}
)
]
});
const tools = server.getTools();
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('greet');
const result = await server.callTool('greet', { name: 'World' });
expect(result.content[0].text).toBe('Hello, World!');
});
});
let client: MCPTestClient;
beforeAll(async () => {
client = await createMCPTestClient({ /* config */ });
});
afterAll(async () => {
await client.disconnect();
});
// Test success
it('should return data for valid input', async () => {
const result = await client.callTool('tool', { valid: true });
expect(result).toMatchMCPToolResponse();
});
// Test failure
it('should reject invalid input', async () => {
await expect(
client.callTool('tool', { invalid: true })
).rejects.toMatchMCPError({ code: -32602 });
});
it('should have properly defined tools', async () => {
const tools = await client.listTools();
tools.forEach(tool => {
expect(tool).toBeValidMCPTool();
expect(tool.description).toBeTruthy();
expect(tool.inputSchema.properties).toBeDefined();
});
});
// ❌ Bad
it('test 1', async () => { /* ... */ });
// ✅ Good
it('should return weather data for valid city names', async () => { /* ... */ });
Each test should be independent and not rely on state from other tests.
// ❌ Bad - Tests depend on order
it('creates a user', async () => { /* ... */ });
it('updates the user', async () => { /* assumes user exists */ });
// ✅ Good - Each test is independent
beforeEach(async () => {
await client.callTool('reset-database', {});
});
it('creates a user', async () => { /* ... */ });
it('updates a user', async () => {
await client.callTool('create-user', { name: 'Test' });
await client.callTool('update-user', { name: 'Updated' });
});
Contributions are welcome! Please feel free to submit a Pull Request.
# Clone the repository
git clone https://github.com/crashbytes/mcp-test-kit.git
cd mcp-test-kit
# Install dependencies
npm install
# Build the project
npm run build
# Run tests
npm test
# Run linter
npm run lint
MIT © Blackhole Software LLC
Built with ❤️ by CrashBytes
FAQs
Testing framework for Model Context Protocol (MCP) servers - Jest/Vitest for MCP
We found that @crashbytes/mcp-test-kit demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.