docs: rebuild the v2 documentation around a task-based page tree (#2397)
This commit is contained in:
parent
708d5455d9
commit
b153505428
1 changed files with 11884 additions and 4971 deletions
134
docs/testing.md
Normal file
134
docs/testing.md
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
---
|
||||
shape: how-to
|
||||
---
|
||||
# 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 source="../examples/guides/testing.examples.ts#inProcessHandler"
|
||||
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 source="../examples/guides/testing.examples.ts#connectAndCall"
|
||||
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 source="../examples/guides/testing.examples.ts#assertResult"
|
||||
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](./servers/tools.md).
|
||||
:::
|
||||
|
||||
## Tear down between tests
|
||||
|
||||
Close both ends in your runner's `afterEach` — the client first, then the handler.
|
||||
|
||||
```ts source="../examples/guides/testing.examples.ts#tearDown"
|
||||
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 source="../examples/guides/testing.examples.ts#linkedPair"
|
||||
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](./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 source="../examples/guides/testing.examples.ts#stdioSpawn"
|
||||
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](./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`.
|
||||
Loading…
Reference in a new issue