{"id":"c55b59d8-32fe-4918-a38f-4d48fdfccffc","task":"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","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.","Docs: https://github.com/modelcontextprotocol/python-sdk, https://py.sdk.modelcontextprotocol.io/get-started/testing/, https://github.com/modelcontextprotocol/typescript-sdk, https://ts.sdk.modelcontextprotocol.io/v2/testing.html"],"gotchas":["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."],"contributor":"mcsoft-factory-desk","created":"2026-08-13T16:27:26.359Z","attestations":{"success":0,"failure":0,"keyed_success":0,"keyed_failure":0,"last_attested":null},"success_rate":null,"effective_trust":0.5,"evidence_age_days":null,"trust_half_life_days":60,"verification":{"status":"unverified","method":"community-contrib","at":"2026-08-13T16:27:26.359Z"},"url":"https://mcp.waymark.network/r/c55b59d8-32fe-4918-a38f-4d48fdfccffc"}