130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
/**
|
|
* Companion example for `docs/testing.md`.
|
|
*
|
|
* Every `ts` fence on that page is synced from a `//#region` in this file
|
|
* (`pnpm sync:snippets --check`). The file also runs: it is the no-socket
|
|
* client harness the page teaches — `createMcpHandler` served through
|
|
* `handler.fetch`, then `InMemoryTransport.createLinkedPair()` — and every
|
|
* output the page quotes verbatim is printed by this program.
|
|
*
|
|
* pnpm --filter @modelcontextprotocol/examples typecheck
|
|
* npx tsx guides/testing.examples.ts # from examples/
|
|
*
|
|
* @module
|
|
*/
|
|
/* eslint-disable no-console */
|
|
|
|
// ## Serve the handler in-process
|
|
|
|
//#region 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))
|
|
});
|
|
//#endregion inProcessHandler
|
|
|
|
// ## Connect a client and call a tool
|
|
|
|
//#region 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);
|
|
//#endregion connectAndCall
|
|
|
|
// Proof for the page's claim that this wiring exercises the real 2026-07-28
|
|
// HTTP path. Throws (non-zero exit) if the claim is false.
|
|
assert.equal(client.getNegotiatedProtocolVersion(), '2026-07-28');
|
|
|
|
// ## Assert on the result
|
|
|
|
//#region 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);
|
|
//#endregion assertResult
|
|
|
|
// ## Tear down between tests
|
|
|
|
//#region tearDown
|
|
await client.close();
|
|
await handler.close();
|
|
//#endregion tearDown
|
|
|
|
// ## Pair two instances in memory
|
|
|
|
//#region 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);
|
|
//#endregion linkedPair
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Harness (not shown on the page). Proves the page's prose claims, then
|
|
// closes the linked pair so the program terminates on its own.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// "The same `callTool` from above returns the same result over this pair."
|
|
const paired = await memClient.callTool({ name: 'apply-discount', arguments: { price: 80, percent: 25 } });
|
|
assert.deepStrictEqual(paired.structuredContent, { total: 60 });
|
|
|
|
// Proof for the page's era caveat: the linked pair runs the 2025 handshake,
|
|
// not the 2026-07-28 revision the `handler.fetch` harness above negotiated.
|
|
assert.equal(memClient.getNegotiatedProtocolVersion(), '2025-11-25');
|
|
|
|
await memClient.close();
|
|
await memServer.close();
|
|
|
|
// ## Cover stdio by spawning the process
|
|
//
|
|
// `StdioClientTransport.start()` spawns a real child process, so this region
|
|
// lives in a wrapper that typechecks but is never called.
|
|
|
|
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
|
|
|
|
async function coverStdio() {
|
|
//#region stdioSpawn
|
|
const stdioClient = new Client({ name: 'test-harness', version: '1.0.0' });
|
|
await stdioClient.connect(new StdioClientTransport({ command: 'node', args: ['dist/server.js'] }));
|
|
//#endregion stdioSpawn
|
|
return stdioClient;
|
|
}
|
|
|
|
void coverStdio;
|
|
|
|
console.log('testing.examples.ts: all assertions passed');
|