MP-201a · Module 2

Testing MCP Tools

4 min read

Testing MCP tools happens at three layers: unit tests for your handler logic, integration tests against the MCP protocol, and end-to-end tests with a real client. Unit tests are the fastest and most important — they test your business logic in isolation, without the MCP transport or SDK in the loop. Extract your tool handlers into pure functions that accept arguments and return results, then test those functions directly with your standard test framework.

Integration testing validates that your server speaks correct MCP protocol. The MCP Inspector (npx @modelcontextprotocol/inspector) is the official tool for this — it connects to your server, lists tools, calls them with test inputs, and shows the raw JSON-RPC messages. Use it during development to verify tool definitions render correctly, inputs are validated, errors return isError: true, and responses are well-formed. It is the MCP equivalent of Postman for REST APIs.

For automated integration tests, the MCP SDK provides an in-memory transport that connects a client and server without stdio or HTTP. Create a Server and a Client, connect them through an InMemoryTransport, and write assertions against the client's responses. This lets you test the full request-response cycle — including schema validation and error handling — in your CI pipeline without spawning processes or opening ports.

import { describe, it, expect } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { InMemoryTransport } from
  "@modelcontextprotocol/sdk/inMemory.js";
import { createServer } from "../src/server.js";

describe("weather tools", () => {
  let client: Client;

  beforeEach(async () => {
    const server = createServer(); // your server factory
    client = new Client(
      { name: "test-client", version: "1.0.0" },
      { capabilities: {} }
    );
    const [clientTransport, serverTransport] =
      InMemoryTransport.createLinkedPair();
    await server.connect(serverTransport);
    await client.connect(clientTransport);
  });

  it("lists tools", async () => {
    const { tools } = await client.listTools();
    expect(tools).toHaveLength(1);
    expect(tools[0].name).toBe("get_weather");
  });

  it("returns weather for valid city", async () => {
    const result = await client.callTool({
      name: "get_weather",
      arguments: { city: "Austin, US", units: "fahrenheit" },
    });
    expect(result.isError).toBeFalsy();
    const data = JSON.parse(result.content[0].text);
    expect(data).toHaveProperty("temperature");
  });

  it("returns error for unknown city", async () => {
    const result = await client.callTool({
      name: "get_weather",
      arguments: { city: "Nonexistentville" },
    });
    expect(result.isError).toBe(true);
    expect(result.content[0].text).toContain("not found");
  });
});
  1. Extract handler logic Move your tool's business logic into a standalone function that accepts typed arguments and returns a result. This function is testable without any MCP infrastructure.
  2. Add protocol tests Use InMemoryTransport to create a client-server pair in your test suite. Write tests for tool listing, successful calls, and error cases. Run these in CI.
  3. Validate with Inspector Run `npx @modelcontextprotocol/inspector` against your server during development. Call each tool with valid and invalid inputs. Check that descriptions, schemas, and error messages render correctly.