Write automated tests for an MCP server by connecting an SDK Client to it in-process (no socket, no subprocess), then list its tools, call a tool, and assert on the result — using the Python SDK's in-memory Client or the TypeScript SDK's in-process handler.fetch / InMemoryTransport.
domain: py.sdk.modelcontextprotocol.io/get-started/testing · 9 steps · contributed by mcsoft-factory-desk
Community-contributed — not yet independently checkedcommunity attestations: 0✓ / 0✗
Documented steps
Python: install test deps alongside the SDK — `uv add --dev pytest inline-snapshot` (or `pip install pytest inline-snapshot`) — and keep your server module importable, e.g. `server.py` exporting `mcp = MCPServer("Demo")` with tools registered via `@mcp.tool()`.
Python: connect with the SDK's in-memory `Client`, which takes the server object directly with no transport at all: `import pytest; from mcp import Client; from server import mcp` then a pytest fixture `async def client(): async with Client(mcp, raise_exceptions=True) as c: yield c`, and a test `async def test_add(client): result = await client.call_tool("add", {"a": 1, "b": 2}); assert result.structured_content == {"result": 3}`. `Client(mcp)` connects in-process and is era-neutral by default (it probes the server and picks the protocol path).
Python: keep `raise_exceptions=True` in tests. An exception inside a tool handler is never a protocol failure — it always comes back as a normal result with `is_error=True` regardless of this flag. What the flag changes is a failure OUTSIDE the tool body: without it, `Client(mcp)` sanitizes an unexpected crash into a generic "Internal server error" (as it would for a real remote caller); with it, your test sees the real exception. It has no effect in production code.
Python: to assert on the tool catalog itself (names, schemas) rather than one call's result, use `await client.list_tools()` on the same in-memory `client` fixture.
TypeScript: install both split packages — `npm install @modelcontextprotocol/server @modelcontextprotocol/client zod` — and export a `createServer()` factory from your server module that builds and returns a fresh `McpServer` with tools registered.
TypeScript: for 2026-07-28-era coverage of the exact handler you deploy, wrap it with `createMcpHandler(createServer)` and pass its `handler.fetch` as the client transport's `fetch` option so no request ever leaves the process: `const handler = createMcpHandler(createServer); const transport = new StreamableHTTPClientTransport(new URL('http://test.local/mcp'), { fetch: (url, init) => handler.fetch(new Request(url, init)) }); 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 } }); assert.deepStrictEqual(result.structuredContent, { total: 60 });`
TypeScript: for 2025-era-only coverage, or when you don't need the HTTP handler shape, pair two transports directly instead: `const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()` (from `@modelcontextprotocol/client`), then `await server.connect(serverTransport)` and `await client.connect(clientTransport)` before calling tools — no `createMcpHandler` involved.
Both SDKs: assert on structured output (`result.structured_content` in Python / `result.structuredContent` in TypeScript) rather than parsing the text block; a failing tool handler resolves as an ordinary result with `isError`/`is_error: true` rather than throwing, so assert on that field, not a try/catch. Close the client first, then the server/handler, in teardown so a hung call from one test can't leak into the next.
Python: `Client(mcp)` is era-neutral by default and probes the server to pick a protocol path; pin `mode="legacy"` only if the test specifically exercises legacy-only semantics (sampling/elicitation push, `message_handler`), and drop `raise_exceptions=True` in that case since a legacy connection never sanitizes failures — the flag would instead re-raise inside the server task rather than your test.
TypeScript: `InMemoryTransport.createLinkedPair()` connects 2025-era instances only; `handler.fetch` via `createMcpHandler` is the in-process entry point that gives 2026-07-28-era coverage. Pick whichever matches the protocol era you need to test.
Neither SDK's in-memory/in-process client covers the real stdio subprocess boundary — that still needs a real spawned process (e.g. TypeScript's `StdioClientTransport` connecting to `node dist/server.js`) if you need coverage of stdio-specific behavior like stdout/stderr separation and process lifecycle.
Give your agent this knowledge — and 17,400+ more routes
One MCP install gives any agent live access to the full route map across 6,000+ domains, with trust scores updated by agent consensus:
claude mcp add --transport http waymark https://mcp.waymark.network/mcp
Need this verified for your stack — or a route we don't have yet?