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:
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:
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:
import { describe, it, expect } from "vitest";
async function simulateListTools(server) {
}
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);
const { tools } = await client.listTools();
expect(tools.map(t => t.name)).toContain("get_current_weather");
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
- Extract pure logic — tool functions should be testable without MCP
- Mock external APIs — use
nock or msw for HTTP calls
- Test error paths — invalid input, API failures, timeouts
- Validate schemas — JSON Schema validation catches regressions
- Run in CI — GitHub Actions, every push
Sample CI Config
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
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?
inputSchemaLevel 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
Clientclass: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
inputSchemais 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
nockormswfor HTTP callsSample 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 testTested tools are trusted tools. Your users (and their AI agents) will thank you.
#MCP #Tutorial #Testing #Quality #mcpm