# Mcp Harness

> In-memory testing toolkit for MCP servers in TypeScript - supertest for MCP

- **Type:** MCP server
- **Install:** `agentstack add mcp-gabry-ts-mcp-harness`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [gabry-ts](https://agentstack.voostack.com/s/gabry-ts)
- **Installs:** 0
- **Category:** [Integrations](https://agentstack.voostack.com/c/integrations)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [gabry-ts](https://github.com/gabry-ts)
- **Source:** https://github.com/gabry-ts/mcp-harness
- **Website:** https://partiti.dev

## Install

```sh
agentstack add mcp-gabry-ts-mcp-harness
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# mcp-harness

[](https://www.npmjs.com/package/mcp-harness)
[](https://github.com/gabry-ts/mcp-harness/actions/workflows/ci.yml)
[](https://opensource.org/licenses/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](https://github.com/ladjs/supertest) but for the Model Context Protocol.

## Quick Start

```ts
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

```bash
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:

```ts
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:

```ts
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:

```ts
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

```ts
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` | Connected harness instance |

### `McpHarness`

| Method | Signature | Description |
|--------|-----------|-------------|
| `listTools()` | `() => Promise` | List all registered tools |
| `callTool()` | `(name, args?) => Promise` | Call a tool by name |
| `listResources()` | `() => Promise` | List all registered resources |
| `readResource()` | `(uri) => Promise` | Read a resource by URI |
| `listResourceTemplates()` | `() => Promise` | List resource templates |
| `listPrompts()` | `() => Promise` | List all registered prompts |
| `getPrompt()` | `(name, args?) => Promise` | Get a prompt by name |
| `ping()` | `() => Promise` | Ping the server |
| `getServerCapabilities()` | `() => ServerCapabilities \| undefined` | Get server capabilities |
| `getServerVersion()` | `() => { name, version } \| undefined` | Get server name and version |
| `close()` | `() => Promise` | 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` | 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

- **[Vitest example](./examples/vitest-example/)** — Full test suite with Vitest
- **[Jest example](./examples/jest-example/)** — Same tests with Jest + ESM
- **[Standalone example](./examples/standalone-example/)** — No test framework, just `node:assert`

## 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.

```mermaid
flowchart LR
  client["MCP Client(harness)"]
  server["McpServer(your code)"]

  client  |InMemoryTransportlinked 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

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

## License

[MIT](./LICENSE) &copy; Gabriele Partiti

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [gabry-ts](https://github.com/gabry-ts)
- **Source:** [gabry-ts/mcp-harness](https://github.com/gabry-ts/mcp-harness)
- **License:** MIT
- **Homepage:** https://partiti.dev

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** yes
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-gabry-ts-mcp-harness
- Seller: https://agentstack.voostack.com/s/gabry-ts
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
