# Test a server

Drive your server through a real `Client`, in-process — no port, no socket, no mock transport.

## Serve the handler in-process

Start from the `createServer` factory you ship — here, one tool — and pass `handler.fetch` as the client transport's `fetch` option.

```ts
import assert from 'node:assert/strict';

import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

function createServer() {
    const server = new McpServer({ name: 'pricing', version: '1.0.0' });
    server.registerTool(
        'apply-discount',
        {
            description: 'Apply a percentage discount to a price',
            inputSchema: z.object({ price: z.number(), percent: z.number().min(0).max(100) }),
            outputSchema: z.object({ total: z.number() })
        },
        async ({ price, percent }) => {
            if (price < 0) {
                return { content: [{ type: 'text', text: 'price must be >= 0' }], isError: true };
            }
            const total = price * (1 - percent / 100);
            return { content: [{ type: 'text', text: `$${total}` }], structuredContent: { total } };
        }
    );
    return server;
}

const handler = createMcpHandler(createServer);

const transport = new StreamableHTTPClientTransport(new URL('http://test.local/mcp'), {
    fetch: (url, init) => handler.fetch(new Request(url, init))
});
```

The transport never dials `http://test.local/mcp` — `handler.fetch` serves every request in-process, through the same `createMcpHandler` you deploy.

## Connect a client and call a tool

Create a `Client`, connect it over the transport, and call the tool. `versionNegotiation: { mode: 'auto' }` negotiates the newest protocol revision the handler serves.

```ts
const client = new Client({ name: 'test-harness', version: '1.0.0' }, { versionNegotiation: { mode: 'auto' } });
await client.connect(transport);

const result = await client.callTool({ name: 'apply-discount', arguments: { price: 80, percent: 25 } });
console.log(result.structuredContent);
```

The handler answered in-process:

```
{ total: 60 }
```

## Assert on the result

Assert on `structuredContent` for the happy path; a handler failure resolves as an ordinary result with `isError: true`, not a thrown error.

```ts
assert.deepStrictEqual(result.structuredContent, { total: 60 });

const failed = await client.callTool({ name: 'apply-discount', arguments: { price: -5, percent: 25 } });
assert.equal(failed.isError, true);
console.log(failed.content);
```

There is nothing to `catch` — `failed.content` carries the message the model would read:

```
[ { type: 'text', text: 'price must be >= 0' } ]
```

::: tip
This page uses `node:assert/strict`; swap in your runner's `expect` — nothing else changes. Arguments the input schema rejects produce the same `isError: true` result, so they assert the same way — see [Tools](https://ts.sdk.modelcontextprotocol.io/v2/servers/tools.md).
:::

## Tear down between tests

Close both ends in your runner's `afterEach` — the client first, then the handler.

```ts
await client.close();
await handler.close();
```

`handler.close()` aborts any exchange still in flight, so a hung tool call cannot leak into the next test.

## Pair two instances in memory

`InMemoryTransport.createLinkedPair()` returns two transports that are each other's wire — connect one instance to each end.

```ts
import { InMemoryTransport } from '@modelcontextprotocol/client';

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

const memServer = createServer();
const memClient = new Client({ name: 'test-harness', version: '1.0.0' });
await memServer.connect(serverTransport);
await memClient.connect(clientTransport);
```

`memClient.callTool` returns the same results over this pair. `createLinkedPair` connects 2025-era instances only; `handler.fetch` is the in-process entry for 2026-07-28 coverage — see [Protocol versions](https://ts.sdk.modelcontextprotocol.io/v2/protocol-versions.md).

## Cover stdio by spawning the process

Stdio has no in-process shortcut: `StdioClientTransport`, imported from `@modelcontextprotocol/client/stdio`, spawns the command and connects to the child over its stdin and stdout.

```ts
const stdioClient = new Client({ name: 'test-harness', version: '1.0.0' });
await stdioClient.connect(new StdioClientTransport({ command: 'node', args: ['dist/server.js'] }));
```

From here the client behaves exactly as above, and `stdioClient.close()` shuts the child process down. [Serve over stdio](https://ts.sdk.modelcontextprotocol.io/v2/serving/stdio.md) covers the server side.

## Recap

- `handler.fetch` passed as the transport's `fetch` option serves every request in-process; the transport never dials the URL.
- One `Client` plus one `createMcpHandler` is a complete no-socket integration test of the server you deploy.
- Assert on `structuredContent`; a handler failure resolves as a result with `isError: true`.
- Close the client, then the handler, between tests.
- `InMemoryTransport.createLinkedPair()` pairs 2025-era instances in memory.
- stdio coverage means spawning the real process with `StdioClientTransport`.
