CoursesAIΒ WorkshopCompaniesPricingBlogConfAiBotSubscribeSignΒ in
  • Courses
  • AIΒ Workshop
  • Companies
  • Pricing
  • Blog
  • ConfAiBot
Subscribe
  • Courses
  • Companies
  • Communities
  • Blog
  • Gift card
  • Newsletter
  • Help
  • Shop
  • ConfAiBot
  • Contact
  • Legal notice
  • General conditions
  • Privacy policy
  • Cookies policy

How to Test MCP Servers

18 September 2025 | resources

MCPs (Model Context Protocol) speedran through the Gartner hype cycle:

  • November 2024: Anthropic announced the MCP specification.
  • February 2025: The term Vibe Coding emerged to describe building software without touching code by relying on AI.
  • February 2025: Cursor added MCP support and surged in adoption.
  • March 2025: People started wiring dozens of MCPs together to vibe-code applications.
  • May - July 2025: Twitter caught fire with security holes uncovered in poorly designed MCPs.
  • August 2025: Things cooled down; many MCPs fell into disuse, but the useful ones stuck.
  • September 2025: The official MCP registry launched, quickly followed by GitHub's own.
Gartner hype cycle showing the stages MCPs went through

The Gartner hype cycle applied to MCPs

In under a year we entered the final stage and started to understand:

  • When it makes sense to use MCPs (and when it does not).
  • How to implement them while following good practices.
  • Why testing is essential to keep them healthy over time.

Why Test MCP Servers

The MCP primitives (tools, resources, prompts, and resource templates) act as entry points into our system, just like HTTP controllers or routes.

When we expose an HTTP endpoint we know we are creating a contract with consumers. We test them to:

  • Ensure they behave as expected.
  • Avoid breaking integrations silently.
  • Document the expected behaviour.
  • Detect regressions quickly.

MCP servers deserve the same treatment; the only difference is that the client is an LLM.

Testing Strategy for MCPs

What Exactly Do We Test?

In our setup HTTP endpoints merely translate the contract into a use case. We do the same with MCPs: they receive the request, call a use case, and return its response.

Within our architecture:

  • HTTP/MCP controllers: Thin translation layers for the protocol.
  • Use cases: Host the business logic (covered by unit tests).

Therefore, the focus of MCP testing is the contract, not the business logic that is already covered elsewhere.

Why Test Them End-to-End

To ensure all pieces fit together we rely on end-to-end tests that:

  • Invoke the MCP server exactly like a real client would.
  • Verify the full stack integration.
  • Hit the database when needed.
  • Guarantee that contracts remain stable.

Testing Tools: CLI vs Library

Why Not Use the MCP Inspector for Testing

While building MCPs, the MCP Inspector is the go-to tool for manual experiments.

The downside is that those checks are manual and become tedious. They also miss all the benefits of automated tests.

The CLI support helps, but as a testing tool it still falls short:

  • Ephemeral connections: It opens and closes the connection on every call.
  • No typing: Responses are untyped.
  • Incomplete API: It does not implement the full MCP specification.

Why the Official MCP Client Works

The most powerful option for automated testing is the official MCP client (TypeScript | Python) because:

  • It keeps persistent connections.
  • It implements the full specification.
  • It is a real client, and the official one.

Test Implementation

Test Organization

We mirror the structure of MCP primitives in the test suite itself:

tests/
β”œβ”€β”€ api/
└── mcp/
    β”œβ”€β”€ courses/
    β”‚   β”œβ”€β”€ prompts/
    β”‚   β”‚   └── SearchSimilarCourseByCoursesNamesPrompt.test.ts
    β”‚   β”œβ”€β”€ resources/
    β”‚   β”‚   β”œβ”€β”€ CourseResourceTemplate.test.ts
    β”‚   β”‚   └── CoursesResource.test.ts
    β”‚   └── tools/
    β”‚       β”œβ”€β”€ SearchAllCoursesTool.test.ts
    β”‚       β”œβ”€β”€ SearchCourseByIdTool.test.ts
    β”‚       β”œβ”€β”€ SearchCourseBySimilarNameTool.test.ts
    β”‚       └── SearchSimilarCoursesByIdsTool.test.ts
    β”œβ”€β”€ users/
    └── videos/

1. Test Client Setup

The first thing we do is instantiate the client, open the connection before the suite runs, and close it afterwards:

describe("SearchCoursesByQueryMcpTool should", () => {
  const mcpClient = new Client(
    {
      name: "mcp-client",
      version: "1.0.0",
    },
    {
      capabilities: {
        resources: {},
        tools: {},
        prompts: {},
      },
    },
  );

  const transport = new StdioClientTransport({
    command: "npx",
    args: ["ts-node", "./src/app/mcp/server.ts"],
  });

  beforeAll(async () => {
    await mcpClient.connect(transport);
  });

  afterAll(async () => {
    await mcpClient.disconnect();
  });
});

2. Validate Primitive Registration

Our first test verifies that the primitives are registered correctly by listing them and asserting that the expected one is present:

it("lists the search course by query tool", async () => {
  const toolsResponse = await mcpClient.listTools();

  expect(toolsResponse.map((response) => response.name))
    .toContain("courses-search_by_query");
});

πŸ’‘ ProTip: Validate just the name, not the description. Descriptions tend to change often and would make the test fragile.

3. Test Primitive Behaviour

From here we test them as if they were HTTP endpoints. In this example the tool receives a query and returns the matching courses.

The key point is testing the contract, so we assert that the entire response matches what we expect:

it("returns an empty array when no courses are found", async () => {
  const response = await mcpClient.callTool("courses-search_by_query", {
    query: "non-existent-course",
    languageCode: "en",
    limit: 5,
  });

  const expectedResponse = { courses: [] };

  expect(response).toEqual({
    content: [
      expect.objectContaining({
        type: "text",
        text: JSON.stringify(expectedResponse),
      }),
    ],
    structuredContent: expectedResponse,
    isError: false,
  });
});

If we need courses on the database, we insert them first:

it("returns existing courses", async () => {
  const query = "Advanced Node";

  const relatedCourse = CourseMother.create({
    title: "Advanced Node",
    summary: "Learn Node.js and master its APIs",
  });
  const lessRelatedCourse = CourseMother.create({
    title: "DDD",
    summary: "Master Domain-Driven Design to craft scalable code",
    languageCode: relatedCourse.languageCode.value,
  });

  await courseRepository.save(relatedCourse);
  await courseRepository.save(lessRelatedCourse);

  const response = await mcpClient.callTool("courses-search_by_query", {
    query,
    languageCode: relatedCourse.languageCode.value,
    limit: 10,
  });

  const expectedResponse = {
    courses: [relatedCourse.toPrimitives(), lessRelatedCourse.toPrimitives()],
  };

  expect(response).toEqual({
    content: [
      expect.objectContaining({
        type: "text",
        text: JSON.stringify(expectedResponse),
      }),
    ],
    structuredContent: expectedResponse,
    isError: false,
  });
});

Resource Template-Specific Tests

Resource templates provide extra capabilities we can also cover with tests.

When a resource template is created it should produce its resources:

it("lists all available course languages as resources", async () => {
  const response = await mcpClient.listResources();

  expect(response.map((resource) => resource.uri))
    .toEqual(expect.arrayContaining(["courses://all?lang=es", "courses://all?lang=en"]));
});

And parameter autocomplete should work too:

it("completes the lang parameter", async () => {
  const response = await mcpClient.completeResourceTemplateParam("courses://all?lang={lang}", "lang", "e");

  expect(response).toEqual(["es", "en"]);
});

Error Handling and Testing

The MCP spec is still young and has inconsistencies, especially around error handling. Tools support expected errors and include isError, but resources do not.

Resource Error Testing

The simplest approach is to wrap the client call in a try/catch (or rely on Jest's rejects) and assert the error:

it("returns a bad request error when the course id is invalid", async () => {
  const invalidId = CourseIdMother.invalid();

  await expect(
    mcpClient.readResource(`courses://${invalidId}`),
  ).rejects.toThrow(
    `MCP error -32000: The id <${invalidId}> is not a valid nano id`,
  );
});

Simplifying the Tests: Our Client Wrapper

The Official Client's Pain Points

The official client aims to be a transparent implementation of the spec, which is perfect for production. For testing, though, we benefit from extra utilities.

To streamline our work we built our wrapper (feel free to drop a star) that extends the official client with testing-friendly helpers.

Advantage #1: Simplified instantiation

const mcpClient = new McpClient("stdio", [
  "npx",
  "ts-node",
  "./src/app/mcp/server.ts",
]);

Advantage #2: Transport swaps without touching tests

Switching from STDIO to HTTP is as simple as changing the constructor:

const mcpClient = new McpClient("http", ["http://localhost:3000/api/mcp"]);

Advantage #3: Testing-specific helpers

it("lists the search course by query tool", async () => {
  const toolsResponse = await mcpClient.listTools();

  expect(toolsResponse.names()).toContain("courses-search_by_query");
});

This helper exposes the names directly, so there is no need to map anything in the assertion.

Best Practices and Recommendations

Must-Have Tests for Each Primitive

  1. Registration test: Ensure the primitive is exposed.
  2. Empty case test: Validate behaviour without data.
  3. Happy path test: Cover the main flow.
  4. Error test: Confirm proper exception handling.
  5. Bug reproduction test: Every new bug should come with a regression test.

Because these tests resemble API tests, you can run them together to guarantee they execute on continuous integration.

Conclusions

Key Takeaways

  • MCPs are contracts: Treat them just like your endpoints.
  • Transport changes must be transparent: Your test suite should not care.
  • Use the right tools: Official client for tests, Inspector for manual work.
  • Do not skip error tests: They matter as much as the happy path.

Additional Resources

  • πŸ“š MCP course: Build your server with best practices: Learn how to build MCP servers properly (in Spanish).
  • πŸ”§ MCP Test Client: Our wrapper that simplifies testing.
  • πŸ“– MCP primitive test examples: Examples from the course for every primitive.

Tags

Software ArchitectureArtificial IntelligenceTesting
SiguienteCodely's First Launch Week Summary

Pay according to your needs

lite (only monthly)

19 €
per month
  • Essential programming courses to build a solid foundation
  • Company invoice
Popular

standard

24,92 €
Save 49 €
Annual payment of 299 €
per month
  • Main catalog to master writing maintainable, scalable, and testable code
  • Receive job offers verified by Codely
  • Company invoice

premium

41,58 €
Save 89 €
Annual payment of 499 €
per month
  • Exclusive courses on AI and emerging technologies to keep you always up to date
  • Early access to new courses
  • Discount on workshops
  • Receive job offers verified by Codely
  • Company invoice

We won't increase the price while your subscription is active