Skip to content

Repository files navigation

vitest-mcp

CI npm license

Vitest harness and matchers for MCP servers.

Test your Model Context Protocol server over the SDK's in-memory transport — no subprocess, no stdio plumbing, no network. Tests run in milliseconds and exercise the real protocol path: initialization handshake, JSON-RPC framing, schema validation. A passing test means your server actually speaks MCP, not just that your handlers return the right shapes.

import { describe, expect, it } from "vitest";
import "vitest-mcp/matchers";
import { connectMCP } from "vitest-mcp";
import { createServer } from "../src/server.js"; // your MCP server factory

describe("my mcp server", () => {
  it("exposes and executes tools", async () => {
    const mcp = await connectMCP(createServer());

    await expect(mcp).toHaveTool("search");

    const result = await mcp.callTool("search", { query: "hello" });
    expect(result).toHaveTextContent(/hello/);
    expect(result).not.toBeToolError();

    await mcp.close();
  });
});

Install

npm i -D vitest-mcp

Works with any server built on @modelcontextprotocol/sdk — both the high-level McpServer and the low-level Server (anything with a connect(transport) method).

Why not just call your handlers?

Unit-testing handler functions skips everything MCP adds: capability negotiation, request/response envelopes, input-schema validation, error mapping. Spawning your server as a subprocess gets you the real thing but costs seconds per test and flakes in CI. The in-memory transport is the middle path: the genuine Client from the SDK, wired directly to your server in the same process.

Matchers

Import once per test file (or in a Vitest setupFiles):

import "vitest-mcp/matchers";
Matcher Asserts
await expect(mcp).toHaveTool(name) server advertises the tool
await expect(mcp).toHaveResource(uri) server advertises the resource
await expect(mcp).toHavePrompt(name) server advertises the prompt
expect(result).toBeToolError() tool result has isError: true
expect(result).toHaveTextContent(str | regex) concatenated text blocks contain/match

Failure messages enumerate what the server actually exposes — a typo'd tool name tells you the fix:

expected server to expose tool "serach" — available tools: search, fetch, summarize

Harness API

const mcp = await connectMCP(server, {
  clientName: "my-test",       // optional handshake identity
  capabilities: {},            // optional client capabilities
});

await mcp.tools();                       // Tool[]
await mcp.callTool("name", { arg: 1 });  // CallToolResult
await mcp.resources();                   // Resource[]
await mcp.resourceTemplates();           // ResourceTemplate[]
await mcp.readResource("uri://…");       // ReadResourceResult
await mcp.prompts();                     // Prompt[]
await mcp.getPrompt("name", { k: "v" }); // GetPromptResult
await mcp.ping();
mcp.client;                              // underlying SDK Client for anything else
await mcp.close();

getTextContent(result) is also exported — the concatenated text of every text content block.

Gotchas this library documents for you

  • Unknown tools don't reject. The SDK returns them as isError results, exactly like handler failures. Assert with toBeToolError(), not .rejects.
  • One server instance, one connection. The in-memory pair links a server to a single client — use a factory (createServer()) and connect a fresh instance per suite.

License

MIT © Binaya Dhakal

About

Vitest harness and matchers for testing MCP servers — in-memory transport, real protocol path, millisecond tests.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages