mcp-harness

npm version CI License: MIT

In-memory testing toolkit for MCP servers in TypeScript — supertest for MCP.

Why

Testing MCP servers today means running them as subprocesses, connecting over stdio, and manually inspecting JSON. It's slow, flaky, and hard to integrate into CI.

mcp-harness gives you a fast, in-memory test harness that connects directly to your McpServer instance — no child processes, no ports, no IO. Just import your server, create a harness, and assert on results. Think supertest but for the Model Context Protocol.

Quick Start

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { createHarness } from 'mcp-harness';
import { hasText, getFirstText } from 'mcp-harness/assertions';

const server = new McpServer({ name: 'my-server', version: '1.0.0' });
server.tool('greet', { name: z.string() }, async ({ name }) => ({
  content: [{ type: 'text', text: `Hello, ${name}!` }],
}));

const harness = await createHarness(server);
const result = await harness.callTool('greet', { name: 'World' });

console.log(getFirstText(result)); // "Hello, World!"
console.log(hasText(result, 'Hello')); // true

await harness.close();

Installation

npm install --save-dev mcp-harness

Peer dependency: @modelcontextprotocol/sdk must be installed in your project.

Usage

In-Memory Mode

Pass your McpServer instance directly. Zero IO, instant startup:

import { createHarness } from 'mcp-harness';
import { myServer } from './server.js';

const harness = await createHarness(myServer);

// List tools, resources, prompts
const tools = await harness.listTools();
const resources = await harness.listResources();
const templates = await harness.listResourceTemplates();
const prompts = await harness.listPrompts();

// Call a tool
const result = await harness.callTool('my-tool', { arg: 'value' });

// Read a resource
const resource = await harness.readResource('info://version');

// Get a prompt
const prompt = await harness.getPrompt('my-prompt', { name: 'Gab' });

// Server info
const capabilities = harness.getServerCapabilities();
const version = harness.getServerVersion();

// Health check
await harness.ping();

// Always close when done
await harness.close();

Subprocess Mode

Test compiled servers via stdio transport, just like a real MCP client would:

import { createHarness } from 'mcp-harness';

const harness = await createHarness({
  command: 'node',
  args: ['dist/server.js'],
  env: { API_KEY: 'test-key' },
  cwd: '/path/to/project',
});

const tools = await harness.listTools();

// Access subprocess stderr for debugging
console.log(harness.stderr);

await harness.close();

Assertion Helpers

Framework-agnostic helpers for inspecting MCP results. Use with Vitest, Jest, node:assert, or any test runner:

import {
  hasText,
  getTexts,
  getFirstText,
  hasError,
  hasErrorMatching,
  toolExists,
  findTool,
  resourceExists,
  promptExists,
} from 'mcp-harness/assertions';

// Inspect tool call results
const result = await harness.callTool('echo', { message: 'hi' });
hasText(result, 'hi'); // true
getFirstText(result); // "hi"
getTexts(result); // ["hi"]

// Check for errors
hasError(result); // false
hasErrorMatching(result, /timeout/); // false

// Inspect server capabilities
const tools = await harness.listTools();
toolExists(tools, 'echo'); // true
findTool(tools, 'echo'); // Tool object or undefined

const resources = await harness.listResources();
resourceExists(resources, 'info://version'); // true

const prompts = await harness.listPrompts();
promptExists(prompts, 'greet'); // true

Options

const harness = await createHarness(server, {
  timeout: 5000, // Connection/request timeout in ms
  clientName: 'my-test', // Client name for MCP handshake
  clientVersion: '1.0.0', // Client version
  clientCapabilities: {}, // Additional client capabilities
});

API Reference

createHarness(server, options?)

Parameter Type Description
server McpServer | SubprocessConfig Server instance or subprocess config
options HarnessOptions Optional connection settings
Returns Promise<McpHarness> Connected harness instance

McpHarness

Method Signature Description
listTools() () => Promise<Tool[]> List all registered tools
callTool() (name, args?) => Promise<CallToolResult> Call a tool by name
listResources() () => Promise<Resource[]> List all registered resources
readResource() (uri) => Promise<ReadResourceResult> Read a resource by URI
listResourceTemplates() () => Promise<ResourceTemplate[]> List resource templates
listPrompts() () => Promise<Prompt[]> List all registered prompts
getPrompt() (name, args?) => Promise<GetPromptResult> Get a prompt by name
ping() () => Promise<void> Ping the server
getServerCapabilities() () => ServerCapabilities | undefined Get server capabilities
getServerVersion() () => { name, version } | undefined Get server name and version
close() () => Promise<void> Close the harness (idempotent)
client Client Raw MCP Client for advanced use
stderr string | undefined Subprocess stderr output (subprocess mode only)

HarnessOptions

Field Type Default Description
timeout number Connection/request timeout in ms
clientName string 'mcp-harness' Client name for handshake
clientVersion string '0.1.0' Client version
clientCapabilities object Additional capabilities

SubprocessConfig

Field Type Description
command string Command to execute ('node', 'npx')
args string[] Command arguments
env Record<string, string> Environment variables (merged with process.env)
cwd string Working directory

Assertion Helpers (mcp-harness/assertions)

Function Signature Description
hasText() (result, text) => boolean Check if any text block contains substring
getTexts() (result) => string[] Extract all text strings from result
getFirstText() (result) => string | undefined Get first text string or undefined
hasError() (result) => boolean Check if result is an error
hasErrorMatching() (result, pattern) => boolean Check error matches string/RegExp
toolExists() (tools, name) => boolean Check if tool exists by name
findTool() (tools, name) => Tool | undefined Find tool by name
resourceExists() (resources, uri) => boolean Check if resource exists by URI
promptExists() (prompts, name) => boolean Check if prompt exists by name

Examples

How It Works

In in-memory mode, mcp-harness uses the MCP SDK's InMemoryTransport to create a linked pair of transports. Your McpServer connects to one side, and a Client connects to the other. Messages flow directly through memory — no serialization, no IO, no child processes.

flowchart LR
  client["MCP Client<br/>(harness)"]
  server["McpServer<br/>(your code)"]

  client <--> |InMemoryTransport<br/>linked pair| server

In subprocess mode, the harness spawns your server as a child process and connects via StdioClientTransport, the same way a real MCP host would.

Contributing

git clone https://github.com/gabry-ts/mcp-harness.git
cd mcp-harness
npm install
npm test
npm run build

License

MIT © Gabriele Partiti