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();
});
});npm i -D vitest-mcpWorks with any server built on @modelcontextprotocol/sdk — both the high-level McpServer and the low-level Server (anything with a connect(transport) method).
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.
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
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.
- Unknown tools don't reject. The SDK returns them as
isErrorresults, exactly like handler failures. Assert withtoBeToolError(), 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.
MIT © Binaya Dhakal