← Back to Blog
tutorialtestingquality

Testing MCP Tools: Unit Tests, Integration Tests, and Mocking

Comprehensive testing strategies for MCP servers — unit test your tools, integration test with real clients.

·4 min read·xapable

Testing MCP Tools: Unit Tests, Integration Tests, and Mocking

MCP tools power AI agents. Bugs can cascade into bad agent decisions. This guide covers testing at every level.

Why Test MCP Tools?

  • Agents depend on correct tool behavior — wrong output = wrong agent actions
  • Schema changes break clients — test your inputSchema
  • Error handling matters — agents need clear error messages
  • Regressions are invisible — without tests, you won't know

Level 1: Unit Testing Tool Handlers

Extract tool logic into testable functions:

// tools.js — pure functions, easy to test
export function addTodo(todos, task, priority = "medium") {
  const todo = {
    id: todos.length + 1,
    task,
    priority,
    created: new Date().toISOString()
  };
  return {
    todos: [...todos, todo],
    message: `✅ Added: "${task}"`
  };
}

export function listTodos(todos, filter = "all") {
  const filtered = filter === "all"
    ? todos
    : todos.filter(t => t.priority === filter);
  return filtered.map(t => `[${t.priority.toUpperCase()}] ${t.task}`);
}

Test with your preferred framework:

// tools.test.js
import { describe, it, expect } from "vitest";
import { addTodo, listTodos } from "./tools.js";

describe("addTodo", () => {
  it("adds a todo with default priority", () => {
    const { todos, message } = addTodo([], "Buy milk");
    expect(todos).toHaveLength(1);
    expect(todos[0].task).toBe("Buy milk");
    expect(todos[0].priority).toBe("medium");
    expect(message).toContain("Buy milk");
  });

  it("adds a todo with custom priority", () => {
    const { todos } = addTodo([], "Fix bug", "high");
    expect(todos[0].priority).toBe("high");
  });
});

describe("listTodos", () => {
  it("filters by priority", () => {
    const todos = [
      { task: "A", priority: "high" },
      { task: "B", priority: "low" }
    ];
    const result = listTodos(todos, "high");
    expect(result).toHaveLength(1);
    expect(result[0]).toContain("A");
  });

  it("returns empty for no matches", () => {
    expect(listTodos([], "high")).toEqual([]);
  });
});

Level 2: Testing the MCP Protocol

Test the request/response cycle:

// server.test.js
import { describe, it, expect } from "vitest";

// Simulate the tools/list handler
async function simulateListTools(server) {
  // You'd need to expose the handler for testing
  // or use the MCP SDK's test utilities
}

describe("tools/list", () => {
  it("returns correct tool schema", async () => {
    const response = await server.handleRequest({
      method: "tools/list",
      params: {}
    });

    expect(response.tools).toHaveLength(2);
    expect(response.tools[0].name).toBe("add_todo");
    expect(response.tools[0].inputSchema.required).toContain("task");
  });
});

describe("tools/call", () => {
  it("add_todo creates a todo", async () => {
    const response = await server.handleRequest({
      method: "tools/call",
      params: {
        name: "add_todo",
        arguments: { task: "Test task", priority: "high" }
      }
    });

    expect(response.content[0].text).toContain("Test task");
  });

  it("unknown tool throws error", async () => {
    await expect(
      server.handleRequest({
        method: "tools/call",
        params: { name: "nonexistent", arguments: {} }
      })
    ).rejects.toThrow("Unknown tool");
  });
});

Level 3: Integration Testing with a Real Client

Use the MCP SDK's Client class:

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: "node",
  args: ["./index.js"],
  env: { OPENWEATHER_API_KEY: "test_key" }
});

const client = new Client({ name: "test", version: "1.0.0" }, {
  capabilities: {}
});

await client.connect(transport);

// List tools
const { tools } = await client.listTools();
expect(tools.map(t => t.name)).toContain("get_current_weather");

// Call a tool
const result = await client.callTool({
  name: "get_current_weather",
  arguments: { city: "London" }
});
expect(result.content[0].text).toContain("London");

Level 4: Schema Validation

Test that your inputSchema is valid JSON Schema:

import Ajv from "ajv";

const ajv = new Ajv();

it("inputSchema is valid JSON Schema", () => {
  for (const tool of TOOLS) {
    const valid = ajv.validateSchema(tool.inputSchema);
    expect(valid).toBe(true);
  }
});

Testing Best Practices

  1. Extract pure logic — tool functions should be testable without MCP
  2. Mock external APIs — use nock or msw for HTTP calls
  3. Test error paths — invalid input, API failures, timeouts
  4. Validate schemas — JSON Schema validation catches regressions
  5. Run in CI — GitHub Actions, every push

Sample CI Config

# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm test

Tested tools are trusted tools. Your users (and their AI agents) will thank you.

#MCP #Tutorial #Testing #Quality #mcpm