MCP Servers

A collection of Model Context Protocol servers, templates, tools and more.

Complete testing & debugging toolkit for MCP (Model Context Protocol) servers

Created 11/2/2025
Updated about 1 month ago
Repository documentation and setup instructions

mcp-dev-kit

npm version CI License: MIT

Complete testing and debugging toolkit for Model Context Protocol (MCP) servers

Build reliable MCP servers with comprehensive testing utilities, intelligent snapshot testing, and stdio-safe debug logging.

Why mcp-dev-kit?

Developing MCP servers comes with unique challenges:

  • Testing is hard - No built-in test utilities for MCP servers
  • Snapshots break - Timestamps and IDs change on every run
  • Logging breaks stdio - console.log() corrupts JSON-RPC communication
  • Manual assertions - Repetitive boilerplate for common checks

mcp-dev-kit solves all of these:

  • MCPTestClient - Full-featured test client for MCP servers
  • Smart snapshots - Auto-exclude dynamic fields (timestamps, IDs)
  • Custom matchers - Readable assertions for tools, resources, prompts
  • Safe logging - Debug without breaking JSON-RPC protocol

Quick Start

Installation

npm install --save-dev mcp-dev-kit vitest

Basic Test Setup

1. Create vitest.setup.ts:

import { installMCPMatchers } from 'mcp-dev-kit/matchers';

installMCPMatchers();

2. Configure vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    setupFiles: ['./vitest.setup.ts'],
  },
});

3. Write tests (server.test.ts):

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { MCPTestClient } from 'mcp-dev-kit/client';

describe('My MCP Server', () => {
  let client: MCPTestClient;

  beforeAll(async () => {
    client = new MCPTestClient({
      command: 'node',
      args: ['./my-server.js'],
    });
    await client.connect();
  });

  afterAll(async () => {
    await client.disconnect();
  });

  it('should list available tools', async () => {
    const tools = await client.listTools();
    expect(tools).toHaveLength(2);
    expect(tools[0]).toHaveToolProperty('name', 'echo');
  });

  it('should execute tools successfully', async () => {
    const result = await client.callTool('echo', { message: 'hello' });
    await expect(result).toReturnToolResult('hello');
  });

  it('should have stable response structure', async () => {
    const result = await client.callTool('list_files', { path: '/' });
    expect(result).toMatchToolResponseSnapshot();
  });
});

4. Add debug logging to your server:

// At the top of your MCP server file
import 'mcp-dev-kit/logger';

// Now console.log works without breaking JSON-RPC!
console.log('Server started');
console.error('Connection error', error);

5. Run tests:

npx vitest

Features

MCP Test Client

Comprehensive test client for spawning and testing MCP servers via stdio.

import { MCPTestClient } from 'mcp-dev-kit/client';

const client = new MCPTestClient({
  command: 'node',
  args: ['./my-server.js'],
  env: { DEBUG: 'true' },
  timeout: 30000,
});

await client.connect();

// Test server capabilities
const serverInfo = client.getServerInfo();
const capabilities = client.getServerCapabilities();

// List and call tools
const tools = await client.listTools();
const result = await client.callTool('my-tool', { param: 'value' });

// List and read resources
const resources = await client.listResources();
const content = await client.readResource('file://config.json');

// List and get prompts
const prompts = await client.listPrompts();
const prompt = await client.getPrompt('greeting', { name: 'Alice' });

// Helper methods for common patterns
const toolResult = await client.expectToolCallSuccess('my-tool', { input: 'test' });
const error = await client.expectToolCallError('bad-tool', {});

await client.disconnect();

Key Features:

  • Automatic process lifecycle management
  • Request/response matching with timeouts
  • Server notification handling
  • Comprehensive error handling
  • TypeScript-first with full type safety

Custom Vitest Matchers

Readable, expressive assertions for MCP-specific testing.

import { installMCPMatchers } from 'mcp-dev-kit/matchers';

installMCPMatchers();

Available Matchers:

// Tool assertions
await expect(client).toHaveTool('echo');
const tools = await client.listTools();
expect(tools[0]).toHaveToolProperty('description', 'Echoes back the message');
expect(tools[0]).toMatchToolSchema({
  type: 'object',
  required: ['message']
});

// Resource assertions
await expect(client).toHaveResource('config://app.json');
const resources = await client.listResources();
expect(resources[0]).toHaveProperty('uri', 'config://app.json');

// Prompt assertions
await expect(client).toHavePrompt('greeting');
const prompts = await client.listPrompts();
expect(prompts[0]).toHaveProperty('name', 'greeting');

// Tool result assertions
const result = await client.callTool('echo', { message: 'test' });
await expect(result).toReturnToolResult('test');
await expect(client.callTool('unknown', {})).toThrowToolError();

Benefits:

  • Clear, self-documenting test code
  • Better error messages when tests fail
  • Reduces boilerplate in test files
  • Type-safe with TypeScript

Snapshot Testing

MCP-aware snapshot testing with intelligent field exclusion.

Why Snapshot Testing for MCP?

MCP server responses often contain dynamic data that changes on every run:

  • Timestamps (2024-11-03T10:30:00.000Z)
  • Request IDs (abc123)
  • Execution times (42.5ms)
  • Auto-increment IDs, file inodes, git SHAs

Regular snapshot testing would fail on every run. mcp-dev-kit automatically excludes these fields while capturing the stable response structure.

Quick Example

import { installMCPMatchers } from 'mcp-dev-kit/matchers';

installMCPMatchers();

describe('File System Server', () => {
  it('should return consistent file listing structure', async () => {
    const result = await client.callTool('list_files', { path: '/project' });

    // Timestamps, IDs, and dynamic fields automatically excluded!
    expect(result).toMatchToolResponseSnapshot();
  });

  it('should have stable tool definitions', async () => {
    const tools = await client.listTools();

    // Captures tool schemas for regression detection
    expect(tools).toMatchToolListSnapshot();
  });

  it('should snapshot custom data structures', async () => {
    const data = {
      users: [...],
      timestamp: new Date().toISOString(), // Auto-excluded
      requestId: 'abc123',                   // Auto-excluded
    };

    expect(data).toMatchMCPSnapshot();
  });
});

Available Snapshot Matchers

toMatchMCPSnapshot(options?) - Generic snapshot matcher for any MCP data

expect(serverResponse).toMatchMCPSnapshot();
expect(data).toMatchMCPSnapshot({ exclude: ['user.id', 'files.*.size'] });

toMatchToolResponseSnapshot(options?) - For tool call results

const result = await client.callTool('query_database', { query: 'SELECT * FROM orders' });
expect(result).toMatchToolResponseSnapshot();

toMatchToolListSnapshot(options?) - For tool definitions

const tools = await client.listTools();
expect(tools).toMatchToolListSnapshot();

toMatchResourceListSnapshot(options?) - For resource listings

const resources = await client.listResources();
expect(resources).toMatchResourceListSnapshot();

toMatchPromptListSnapshot(options?) - For prompt definitions

const prompts = await client.listPrompts();
expect(prompts).toMatchPromptListSnapshot();

Smart Defaults

These fields are automatically excluded from all snapshots:

  • timestamp
  • requestId
  • executionTime
  • cacheKey
  • _meta.timestamp
  • serverInfo.startedAt
  • serverInfo.uptime

Example:

// Original response
{
  "users": [...],
  "timestamp": "2024-11-03T10:30:00.000Z",  // ❌ Excluded
  "requestId": "abc123",                     // ❌ Excluded
  "executionTime": 42.5                      // ❌ Excluded
}

// Snapshot (only stable data)
{
  "users": [...]  // ✅ Captured
}

Custom Exclusions

Exclude additional fields using glob patterns:

// Exclude file system-specific fields
expect(result).toMatchToolResponseSnapshot({
  exclude: ['files.*.size', 'files.*.inode', 'files.*.modified']
});

// Exclude all auto-increment IDs
expect(data).toMatchMCPSnapshot({
  exclude: ['*.id', '*.userId', 'rows.*.orderId']
});

// Exclude nested timestamps with custom names
expect(response).toMatchMCPSnapshot({
  exclude: ['data.users.*.createdAt', 'metadata.generatedAt']
});

Pattern Syntax:

  • field - Excludes top-level field
  • nested.field - Excludes nested field
  • array.*.field - Excludes field from all array items
  • data.users.*.createdAt - Excludes createdAt from all users in data.users

Performance

Snapshot testing with property exclusion adds negligible overhead:

| Data Size | Normalization Overhead | Notes | |-----------|----------------------|-------| | 10-100 items | < 50 microseconds | Typical MCP responses | | 1000 items | < 50 microseconds | Large responses | | 5000 items | < 50 microseconds | Extra-large responses | | Deep nesting (10+ levels) | ~1-2 milliseconds | Rare in practice |

Performance Characteristics:

  • Scales well with data SIZE - More items ≈ similar overhead
  • Degrades with NESTING DEPTH - Deeper structures = slower
  • Production-ready - < 1% overhead for typical MCP responses

Note on Benchmarks: Our benchmarks measure JIT-optimized code after warmup. Real-world "cold start" performance may vary slightly. All measurements exclude snapshot file I/O (handled by Vitest).

Feedback Welcome! I'm actively testing this feature and would love your feedback! If you experience performance issues or have suggestions, please open an issue on GitHub.

Updating Snapshots

When you intentionally change your server's response format:

# Review what changed
npm test

# Update snapshots after verifying changes are correct
npm test -- -u

Best Practices

✅ DO:

  • Snapshot server response structure to catch regressions
  • Use smart defaults for common dynamic fields
  • Snapshot small-to-medium datasets (10-1000 items)
  • Combine snapshots with explicit assertions for critical properties
  • Review snapshot diffs before accepting changes
  • Split large responses into focused, smaller snapshots

❌ DON'T:

  • Snapshot without excluding dynamic fields (timestamps, IDs, etc.)
  • Create multi-megabyte snapshots (split into smaller tests instead)
  • Snapshot implementation details that may change frequently
  • Blindly update snapshots with -u flag without reviewing
  • Use snapshots as a replacement for explicit assertions

Example: Combined Approach

it('should return valid user list', async () => {
  const result = await client.callTool('list_users', {});
  const parsed = JSON.parse(result.content[0]?.text || '{}');

  // Explicit assertions for critical properties
  expect(parsed.users).toHaveLength(50);
  expect(parsed.users[0]).toHaveProperty('name');
  expect(parsed.users[0]).toHaveProperty('email');

  // Snapshot for structure regression detection
  expect(result).toMatchToolResponseSnapshot();
});

See examples/snapshot-example/ for complete working examples with benchmarks.

Debug Logging

Safe debug logging that doesn't break JSON-RPC stdio communication.

The Problem

MCP servers communicate via JSON-RPC over stdio. Every message must be a single line of JSON on stdout:

{"jsonrpc":"2.0","method":"tools/list","params":{...}}\n

If you write anything else to stdout (like console.log()), it corrupts the stream:

Server starting...  ← Breaks protocol!
{"jsonrpc":"2.0","method":"tools/list","params":{...}}\n

Result: SyntaxError: Unexpected token 'S'

The Solution

mcp-dev-kit redirects all console output to stderr, keeping stdout clean:

  • stdout = pure JSON-RPC (protocol)
  • stderr = all your logs (debugging)

Auto-Patch (Recommended)

// At the top of your MCP server file
import 'mcp-dev-kit/logger';

// Now console.log works without breaking JSON-RPC!
console.log('Server started', { port: 3000 });
console.info('Configuration loaded');
console.warn('Deprecated feature used');
console.error('Connection failed', error);

Opt-out:

MCP_DEV_KIT_NO_AUTO_PATCH=true node server.js

Manual Logger

For more control, create a custom logger instance:

import { createLogger } from 'mcp-dev-kit';

const logger = createLogger({
  timestamps: true,
  colors: true,
  level: 'info', // Only show info and above
  logFile: './server.log', // Optional file output
});

logger.info('Server starting...');
logger.warn('Configuration may need updating');
logger.error('Connection failed', { reason: 'timeout' });

// Cleanup when done
await logger.close();

Logger Features

  • Auto-patching - Just import and console.log works
  • Colored output - Color-coded log levels (auto-detects TTY)
  • Timestamps - ISO8601 timestamps on all logs
  • Object formatting - Pretty-print objects with util.inspect()
  • File logging - Optional async file output
  • Cleanup - Graceful restoration of original console
  • Zero overhead - Lightweight, uses picocolors (7 KB)

Configuration

Log Levels:

const logger = createLogger({ level: 'warn' });

logger.debug('Not shown');
logger.info('Not shown');
logger.warn('Shown');     // ✓
logger.error('Shown');    // ✓

Colors:

createLogger({ colors: false }); // Force disable
createLogger({ colors: true });  // Force enable
// Auto-detected by default based on process.stderr.isTTY

Timestamps:

createLogger({ timestamps: false }); // Disable
// ISO8601 format: 2024-11-03T12:34:56.789Z

File Logging:

const logger = createLogger({
  logFile: './server.log',
});

logger.info('This goes to both stderr and server.log');

// Flush pending writes
await logger.close();

Testing Guidelines

Test Structure

Organize your tests by MCP capabilities:

describe('My MCP Server', () => {
  let client: MCPTestClient;

  beforeAll(async () => {
    client = new MCPTestClient({ command: 'node', args: ['./server.js'] });
    await client.connect();
  });

  afterAll(async () => {
    await client.disconnect();
  });

  describe('Server Initialization', () => {
    it('should expose correct server info', () => {
      const info = client.getServerInfo();
      expect(info.name).toBe('my-server');
      expect(info.version).toBe('1.0.0');
    });

    it('should declare required capabilities', () => {
      const caps = client.getServerCapabilities();
      expect(caps.tools).toBeDefined();
    });
  });

  describe('Tools', () => {
    it('should list all available tools', async () => {
      const tools = await client.listTools();
      expect(tools).toHaveLength(3);
      expect(tools.map(t => t.name)).toEqual(['echo', 'calculate', 'search']);
    });

    it('should execute tools successfully', async () => {
      const result = await client.callTool('echo', { message: 'test' });
      expect(result.content[0]?.text).toBe('test');
    });

    it('should handle tool errors gracefully', async () => {
      const error = await client.expectToolCallError('calculate', { invalid: 'params' });
      expect(error.message).toContain('Invalid parameters');
    });

    it('should have stable tool schemas', async () => {
      const tools = await client.listTools();
      expect(tools).toMatchToolListSnapshot();
    });
  });

  describe('Resources', () => {
    it('should list available resources', async () => {
      await expect(client).toHaveResource('config://app.json');
    });

    it('should read resource content', async () => {
      const content = await client.readResource('config://app.json');
      expect(content.contents[0]?.text).toContain('version');
    });
  });

  describe('Prompts', () => {
    it('should provide defined prompts', async () => {
      await expect(client).toHavePrompt('greeting');
    });

    it('should render prompts with arguments', async () => {
      const prompt = await client.getPrompt('greeting', { name: 'Alice' });
      expect(prompt.messages[0]?.content.text).toContain('Alice');
    });
  });
});

Testing Best Practices

✅ DO:

  1. Test all MCP capabilities - Tools, resources, prompts
  2. Use descriptive test names - Clearly state what's being tested
  3. Combine matchers and snapshots - Explicit assertions + structure validation
  4. Test error cases - Don't just test happy paths
  5. Clean up resources - Always disconnect client in afterAll
  6. Use timeouts appropriately - Set reasonable timeouts for slow operations
  7. Test server lifecycle - Test initialization and shutdown

❌ DON'T:

  1. Don't share client state - Each test suite should have its own client
  2. Don't skip error testing - Error handling is critical
  3. Don't test implementation details - Test public API only
  4. Don't create flaky tests - Avoid timing-dependent assertions
  5. Don't ignore snapshots - Review snapshot changes carefully
  6. Don't hardcode system-specific paths - Use relative paths or env vars

Error Testing

Always test error conditions:

it('should validate tool parameters', async () => {
  const error = await client.expectToolCallError('calculate', {
    // Missing required parameter
  });
  expect(error.code).toBe(-32602); // Invalid params
  expect(error.message).toContain('Required parameter');
});

it('should handle resource not found', async () => {
  await expect(
    client.readResource('nonexistent://resource')
  ).rejects.toThrow('Resource not found');
});

it('should reject unknown tools', async () => {
  await expect(
    client.callTool('unknown-tool', {})
  ).rejects.toThrow();
});

Performance Testing

Test response times for critical operations:

it('should respond quickly to tool calls', async () => {
  const start = Date.now();
  await client.callTool('quick-operation', {});
  const duration = Date.now() - start;

  expect(duration).toBeLessThan(1000); // < 1 second
});

Integration Testing

Test real-world workflows:

it('should handle complete user workflow', async () => {
  // 1. List available tools
  const tools = await client.listTools();
  expect(tools.length).toBeGreaterThan(0);

  // 2. Get resource for context
  const config = await client.readResource('config://app.json');
  const settings = JSON.parse(config.contents[0]?.text || '{}');

  // 3. Execute tool with context
  const result = await client.callTool('process', {
    mode: settings.defaultMode,
  });
  expect(result.content[0]?.text).toBeTruthy();

  // 4. Verify result structure
  expect(result).toMatchToolResponseSnapshot();
});

Examples

See examples/ directory for complete examples:

Run examples:

npm install -g tsx
tsx examples/logger/basic-usage.ts

API Reference

MCPTestClient

class MCPTestClient {
  constructor(options: {
    command: string;
    args?: string[];
    env?: Record<string, string>;
    timeout?: number;
  });

  // Connection management
  connect(): Promise<void>;
  disconnect(): Promise<void>;

  // Server info
  getServerInfo(): ServerInfo;
  getServerCapabilities(): ServerCapabilities;

  // Tools
  listTools(): Promise<Tool[]>;
  callTool(name: string, args: unknown): Promise<CallToolResult>;
  expectToolCallSuccess(name: string, args: unknown): Promise<string>;
  expectToolCallError(name: string, args: unknown): Promise<Error>;

  // Resources
  listResources(): Promise<Resource[]>;
  readResource(uri: string): Promise<ReadResourceResult>;

  // Prompts
  listPrompts(): Promise<Prompt[]>;
  getPrompt(name: string, args?: unknown): Promise<GetPromptResult>;
}

Logger

interface LoggerOptions {
  enabled?: boolean;        // Enable/disable logger (default: true)
  timestamps?: boolean;     // Show timestamps (default: true)
  colors?: boolean;         // Force colors on/off (default: auto-detect)
  level?: 'debug'|'info'|'warn'|'error'; // Min level (default: 'debug')
  stream?: WritableStream;  // Custom output (default: process.stderr)
  logFile?: string;         // Optional file output
}

function createLogger(options?: LoggerOptions): DebugLogger;
function patchConsole(options?: LoggerOptions): void;
function unpatchConsole(): void;

Matchers

// Installation
function installMCPMatchers(): void;

// Tool matchers
expect(client).toHaveTool(name: string);
expect(tool).toHaveToolProperty(property: string, value?: any);
expect(tool).toMatchToolSchema(schema: object);
expect(result).toReturnToolResult(expected: any);
expect(promise).toThrowToolError();

// Resource matchers
expect(client).toHaveResource(uri: string);

// Prompt matchers
expect(client).toHavePrompt(name: string);

// Snapshot matchers
expect(data).toMatchMCPSnapshot(options?: { exclude?: string[] });
expect(result).toMatchToolResponseSnapshot(options?: { exclude?: string[] });
expect(tools).toMatchToolListSnapshot(options?: { exclude?: string[] });
expect(resources).toMatchResourceListSnapshot(options?: { exclude?: string[] });
expect(prompts).toMatchPromptListSnapshot(options?: { exclude?: string[] });

Troubleshooting

Tests hanging or timing out?

Increase the timeout:

const client = new MCPTestClient({
  command: 'node',
  args: ['./server.js'],
  timeout: 60000, // 60 seconds
});

Snapshots failing unexpectedly?

Check if you're excluding enough dynamic fields:

expect(result).toMatchToolResponseSnapshot({
  exclude: [
    'timestamp',
    'requestId',
    'files.*.modified',
    'data.*.generatedAt',
  ]
});

Logs not showing?

Check your log level:

createLogger({ level: 'debug' }); // Show everything

Colors not working?

Colors only work when stderr is a TTY. Force enable/disable:

createLogger({ colors: true });  // Always color
createLogger({ colors: false }); // Never color

Client not connecting?

Verify your server is using stdio transport and responding to initialize:

// Server must respond to initialize request
server.setRequestHandler(InitializeRequestSchema, async (request) => {
  return {
    protocolVersion: '2024-11-05',
    capabilities: { tools: {} },
    serverInfo: { name: 'my-server', version: '1.0.0' },
  };
});

Requirements

  • Node.js >= 18.0.0
  • TypeScript >= 5.0.0 (if using TypeScript)
  • Vitest >= 1.0.0 (for testing features)

Contributing

See CONTRIBUTING.md for development setup and guidelines.

License

MIT © gnana997

Related Projects


Built with ❤️ for the MCP community

Found this useful? Star it on GitHub

Quick Setup
Installation guide for this server

Install Package (if required)

npx @modelcontextprotocol/server-mcp-dev-kit

Cursor configuration (mcp.json)

{ "mcpServers": { "gnana997-mcp-dev-kit": { "command": "npx", "args": [ "gnana997-mcp-dev-kit" ] } } }