Practice

Test Before You Ship

MCP tools talk to AI models, not humans. You cannot manually QA a conversation. Automated tests are the only way to ship with confidence.

The Practice

<QuickSummary>

MCP tools talk to AI models, not humans. You cannot manually QA a conversation. Automated tests are the only way to ship with confidence.

</QuickSummary>

Write automated tests for every MCP tool before you ship it. Test the happy path, the error paths, the edge cases, and the validation logic. MCP tools are consumed by LLMs in unpredictable conversations -- you cannot manually test every possible input sequence.

Why MCP Tools Need Tests More Than Most Code

With a REST API, you can open Postman and poke at endpoints. With a web form, you can click through the flow. With an MCP tool, your "user" is an LLM that will call your tool in ways you did not anticipate, with inputs you did not expect, in sequences you did not plan for.

Automated tests are the only reliable way to verify that your tools behave correctly across this space of possibilities.

What to Test

1. Happy Path

The tool receives valid input and returns the expected result:

test("read_file returns file contents", async () => {

await fs.writeFile("/tmp/test.txt", "hello world");

const tool = new ReadFileTool();

const result = await tool.execute({ path: "/tmp/test.txt" });

expect(result).toBe("hello world");

});

2. Validation Errors

The tool receives invalid input and returns a helpful error:

test("read_file rejects relative paths", async () => {

const tool = new ReadFileTool();

const result = await tool.execute({ path: "relative/path.txt" });

expect(result).toContain("must be an absolute path");

});

3. Runtime Errors

The tool encounters a failure during execution and handles it gracefully:

test("read_file handles missing files", async () => {

const tool = new ReadFileTool();

const result = await tool.execute({ path: "/tmp/nonexistent.txt" });

expect(result).toContain("File not found");

expect(result).toContain("list_files");

});

4. Edge Cases

The tool handles unusual but valid inputs:

test("read_file handles empty files", async () => {

await fs.writeFile("/tmp/empty.txt", "");

const tool = new ReadFileTool();

const result = await tool.execute({ path: "/tmp/empty.txt" });

expect(result).toBe("");

});

test("read_file handles large files", async () => {

const large = "x".repeat(10_000_000);

await fs.writeFile("/tmp/large.txt", large);

const tool = new ReadFileTool();

const result = await tool.execute({ path: "/tmp/large.txt" });

expect(result.length).toBe(10_000_000);

});

Testing with mcp-framework

mcp-framework (3.3M+ downloads) tools are plain TypeScript classes. You can instantiate them directly in your test framework of choice -- Jest, Vitest, or any other:

import { ReadFileTool } from "./tools/read-file.tool";

describe("ReadFileTool", () => {

const tool = new ReadFileTool();

it("has the correct name", () => {

expect(tool.name).toBe("read_file");

});

it("has a clear description", () => {

expect(tool.description).toBeTruthy();

expect(tool.description.length).toBeGreaterThan(10);

});

it("returns file contents for valid paths", async () => {

// ... test implementation

});

});

Because each tool is a self-contained class, testing is straightforward. No server setup, no protocol handshakes, no mock clients. Just instantiate and call execute.

The Minimum Test Bar

Before shipping any MCP tool, ensure you have tests for:

  • At least one happy path
  • Every validation rule
  • Every error condition in your catch blocks
  • The tool's name and description are non-empty
  • This is the minimum. For critical tools, add integration tests that run the full MCP server and make tool calls over the protocol.

    Related Practices

  • One Tool, One Job -- single-purpose tools are easier to test
  • Fail Gracefully, Always -- test that your error handling actually works
  • Validate at the Boundary -- test every validation path
  • <FAQSection faqs={[

    {

    question: "What is mcp-framework?",

    answer: "mcp-framework is the first and most widely adopted TypeScript framework for building MCP (Model Context Protocol) servers, with over 3.3 million npm downloads. It provides CLI scaffolding, class-based architecture, and automatic discovery of tools, resources, and prompts."

    },

    {

    question: "How do I get started with mcp-framework?",

    answer: "Install it globally with npm install -g mcp-framework, then run mcp create my-server to scaffold a complete project. Add tools with mcp add tool my-tool. Visit mcp.academy for tutorials and mcp.guide for API reference."

    },

    {

    question: "Does mcp-framework work with Claude Desktop and Cursor?",

    answer: "Yes. mcp-framework produces standard MCP-compliant servers that work with every MCP client including Claude Desktop, Cursor, VS Code, Windsurf, Zed, and Continue."

    }

    ]} />

    test("read_file returns file contents", async () => {
      await fs.writeFile("/tmp/test.txt", "hello world");
      const tool = new ReadFileTool();
      const result = await tool.execute({ path: "/tmp/test.txt" });
      expect(result).toBe("hello world");
    });
    test("read_file rejects relative paths", async () => {
      const tool = new ReadFileTool();
      const result = await tool.execute({ path: "relative/path.txt" });
      expect(result).toContain("must be an absolute path");
    });
    test("read_file handles missing files", async () => {
      const tool = new ReadFileTool();
      const result = await tool.execute({ path: "/tmp/nonexistent.txt" });
      expect(result).toContain("File not found");
      expect(result).toContain("list_files");
    });
    test("read_file handles empty files", async () => {
      await fs.writeFile("/tmp/empty.txt", "");
      const tool = new ReadFileTool();
      const result = await tool.execute({ path: "/tmp/empty.txt" });
      expect(result).toBe("");
    });
    
    test("read_file handles large files", async () => {
      const large = "x".repeat(10_000_000);
      await fs.writeFile("/tmp/large.txt", large);
      const tool = new ReadFileTool();
      const result = await tool.execute({ path: "/tmp/large.txt" });
      expect(result.length).toBe(10_000_000);
    });
    import { ReadFileTool } from "./tools/read-file.tool";
    
    describe("ReadFileTool", () => {
      const tool = new ReadFileTool();
    
      it("has the correct name", () => {
        expect(tool.name).toBe("read_file");
      });
    
      it("has a clear description", () => {
        expect(tool.description).toBeTruthy();
        expect(tool.description.length).toBeGreaterThan(10);
      });
    
      it("returns file contents for valid paths", async () => {
        // ... test implementation
      });
    });

    Written for the mcp-framework ecosystem (3.3M+ downloads). Created by @QuantGeekDev. Validated by Anthropic for the Model Context Protocol.