# How to Write Tests for MCP Server Tools: A Complete Guide

> Learn how to write tests for MCP server tools by mocking McpServer, capturing handlers, and invoking them with JSON inputs to check return structures. A complete guide.

- Repository: [Model Context Protocol/servers](https://github.com/modelcontextprotocol/servers)
- Tags: how-to-guide
- Published: 2026-03-01

---

**You can test MCP server tools by mocking the `McpServer` instance, capturing the registered handler functions, and invoking them directly with JSON inputs to verify their return structures.**

Model Context Protocol (MCP) servers expose functionality to LLMs through **tools** – small, well-defined operations registered via `server.registerTool(name, config, handler)`. Because these tools are pure functions that accept JSON-serializable arguments and return deterministic responses, you can unit test them without running a live MCP daemon. The canonical reference for this pattern is the test suite located at **[`src/everything/__tests__/tools.test.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/everything/__tests__/tools.test.ts)** in the official servers repository.

## Understanding the MCP Tool Testing Pattern

The testing strategy follows a four-step pattern used consistently across the codebase:

1. **Create a mock `McpServer`** using a plain object with spied methods.
2. **Capture the handler** that `registerTool` receives in a Map for later invocation.
3. **Invoke the handler** with expected input arguments and optional mock request metadata.
4. **Assert the returned structure**, verifying the content array shape and any side effects like logging or notifications.

This approach isolates tool logic from transport concerns, allowing you to test validation, business logic, and side effects independently.

## Setting Up the Test Environment

### Creating a Mock McpServer

The foundation of every tool test is a helper function that constructs a typed mock server. This mock must expose `registerTool` and optionally `registerResource`, `getClientCapabilities`, and notification methods.

```typescript
import { describe, it, expect, vi } from 'vitest';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';

function createMockServer() {
  const handlers = new Map<string, Function>();
  
  const mockServer = {
    registerTool: vi.fn((name: string, _config: any, handler: Function) => {
      handlers.set(name, handler);
    }),
    registerResource: vi.fn(),
    server: { 
      getClientCapabilities: vi.fn(() => ({})), 
      notification: vi.fn() 
    },
    sendLoggingMessage: vi.fn(),
    sendResourceUpdated: vi.fn(),
  } as unknown as McpServer;
  
  return { mockServer, handlers };
}

```

This helper captures every registered handler in a Map, allowing tests to retrieve and invoke them directly without going through the MCP protocol layer.

## Testing Basic Tool Handlers

### Testing the Echo Tool

The `echo` tool demonstrates the simplest case: a handler that returns text content based on input arguments.

```typescript
import { registerEchoTool } from '../tools/echo.js';

describe('echo tool', () => {
  it('should echo back the message', async () => {
    const { mockServer, handlers } = createMockServer();
    registerEchoTool(mockServer);
    
    const handler = handlers.get('echo')!;
    const result = await handler({ message: 'Hello, World!' });
    
    expect(result).toEqual({ 
      content: [{ type: 'text', text: 'Echo: Hello, World!' }] 
    });
  });
});

```

This test verifies that the handler correctly structures its return value with a **content array** containing a text object, which is the required MCP tool response format.

## Testing Input Validation and Error Handling

### Validating Tool Input (get-sum)

Tools often enforce schemas using Zod or similar validators. Test these boundaries by passing invalid inputs and asserting that the handler throws appropriate errors.

```typescript
import { registerGetSumTool } from '../tools/get-sum.js';

it('rejects invalid input', async () => {
  const { mockServer, handlers } = createMockServer();
  registerGetSumTool(mockServer);
  const handler = handlers.get('get-sum')!;

  await expect(handler({})).rejects.toThrow();
  await expect(handler({ a: 'x', b: 5 })).rejects.toThrow();
  await expect(handler({ a: 1 })).rejects.toThrow();
});

```

These assertions ensure that the tool's **input validation** correctly rejects malformed arguments, missing required fields, and type mismatches before executing business logic.

## Testing Tools with Side Effects

### Simulated Logging Toggle (toggle-simulated-logging)

Some tools interact with server state or trigger notifications. Capture these side effects by spying on mock server methods like `sendLoggingMessage`.

```typescript
import { registerToggleSimulatedLoggingTool } from '../tools/toggle-simulated-logging.js';

it('starts and stops logging correctly', async () => {
  const { mockServer, handlers } = createMockServer();
  registerToggleSimulatedLoggingTool(mockServer);
  const handler = handlers.get('toggle-simulated-logging')!;

  // First call starts logging
  const start = await handler({}, { sessionId: 'sess-1' });
  expect(start.content[0].text).toContain('Started');

  // Second call stops logging
  const stop = await handler({}, { sessionId: 'sess-1' });
  expect(stop.content[0].text).toContain('Stopped');
});

```

This pattern tests **stateful behavior** by invoking the same handler twice and verifying that the internal state toggles between calls, while also demonstrating how to pass **request metadata** (like `sessionId`) as a second argument.

## Testing Conditional Tool Registration

### Client Capability Guards (sampling)

Advanced tools like sampling or elicitation only register if the client advertises support via capabilities. Mock `getClientCapabilities` to test these conditional paths.

```typescript
import { registerTriggerSamplingRequestTool } from '../tools/trigger-sampling-request.js';

it('registers only when client supports sampling', () => {
  const handlers = new Map<string, Function>();
  const mockServer = {
    registerTool: vi.fn((name, _cfg, h) => handlers.set(name, h)),
    server: { 
      getClientCapabilities: vi.fn(() => ({ sampling: {} })) 
    },
  } as unknown as McpServer;

  registerTriggerSamplingRequestTool(mockServer);
  
  expect(mockServer.registerTool).toHaveBeenCalledWith(
    'trigger-sampling-request',
    expect.objectContaining({ title: expect.any(String) }),
    expect.any(Function)
  );
});

```

This test verifies that the tool correctly checks for **client capabilities** before registration, ensuring compatibility with the MCP protocol's capability negotiation system.

## Testing Resource-Producing Tools

### Gzip File Resource (gzip-file-as-resource)

Tools that generate dynamic resources require verifying both the return structure and any calls to `registerResource`.

```typescript
import { registerGZipFileAsResourceTool } from '../tools/gzip-file-as-resource.js';

it('compresses a data URI into a resource link', async () => {
  const mockServer = {
    registerTool: vi.fn(),
    registerResource: vi.fn(),
  } as unknown as McpServer;

  let handler: Function | null = null;
  (mockServer.registerTool as any).mockImplementation((_n, _c, h) => (handler = h));

  registerGZipFileAsResourceTool(mockServer);

  const dataUri = `data:text/plain;base64,${Buffer.from('test').toString('base64')}`;
  const result = await handler!({ 
    name: 'test.txt.gz', 
    data: dataUri, 
    outputType: 'resourceLink' 
  });

  expect(result.content[0].type).toBe('resource_link');
  expect(result.content[0].uri).toContain('test.txt.gz');
});

```

This demonstrates testing **binary data processing** and **resource registration**, verifying that the tool correctly transforms input data and returns a properly formatted resource link.

## Summary

- **Mock the server** using a plain object with `vi.fn()` spies for `registerTool`, `registerResource`, and notification methods to isolate unit tests from transport concerns.
- **Capture handlers** in a Map during registration so you can invoke them directly with JSON inputs without running an MCP daemon.
- **Assert content structure** by verifying that handlers return objects with a `content` array (or `structuredContent`) containing properly typed elements.
- **Test validation logic** by passing invalid inputs and asserting that handlers throw errors or reject promises.
- **Verify side effects** by checking calls to `sendLoggingMessage`, `notification`, or internal state changes when testing stateful tools.
- **Check conditional registration** by mocking `getClientCapabilities` to ensure tools only register when the client supports specific features like sampling.

## Frequently Asked Questions

### Do I need a running MCP server to test tools?

No. According to the `modelcontextprotocol/servers` source code, you can test tools in complete isolation by mocking the `McpServer` instance and capturing the handler functions registered via `registerTool`. This allows you to invoke tool logic directly without establishing an MCP transport connection or running a daemon.

### How do I mock client capabilities in tests?

Create a mock server object that includes a `server.getClientCapabilities` method returning the specific capability flags you want to test. For example, return `{ sampling: {} }` to test sampling tools or `{}` to test behavior when capabilities are absent. Then assert whether `registerTool` was called based on the capability check.

### What should I assert when testing an MCP tool?

Assert three specific things: first, that the return value contains a `content` array with objects having `type` and appropriate content fields (like `text` or `uri`); second, that input validation correctly rejects malformed arguments by throwing errors; and third, that any side effects—such as calls to `sendLoggingMessage` or state toggles—occur as expected.

### How do I test tools that interact with external APIs?

Follow the same mock server pattern, but additionally mock the external API clients or HTTP requests within the tool's handler. Since you capture the handler via `registerTool`, you can use standard mocking libraries like `vi.fn()` or `jest.mock()` to intercept HTTP calls, then invoke the handler and assert that it processes the mocked API responses correctly into the MCP content format.