MCP: Testing & Debugging
Test at four layers, from fastest to slowest.
| Layer | Tool | Catches |
|---|---|---|
| 1. Unit | Your test runner (Vitest, Jest, pytest) | Logic bugs in handlers |
| 2. Protocol | qa/mcp-smoke.mjs, SDK in-memory client |
Handshake, schemas, tool registration, stdout pollution |
| 3. Interactive | MCP Inspector | Descriptions, schemas, and error messages as the model sees them |
| 4. End-to-end | A real host (Cursor, Claude Code) + eval prompts | Whether the model picks the right tool and uses it correctly |
1. Unit tests
Section titled “1. Unit tests”Keep handler logic in plain functions and test them directly:
// src/notes.ts exports searchNotes(notes, query, limit); the tool handler just calls itimport { searchNotes } from "../src/notes.js";test("search is case-insensitive", () => { expect(searchNotes([{ title: "MCP", body: "", created: "" }], "mcp", 5)).toHaveLength(1);});2. Protocol smoke test (automated)
Section titled “2. Protocol smoke test (automated)”This guide includes a dependency-free smoke tester, qa/mcp-smoke.mjs, that launches any stdio server, performs the MCP handshake, lists the tools, and optionally calls one:
node qa/mcp-smoke.mjs <command> [args...]node qa/mcp-smoke.mjs node 03-mcp/templates/typescript-server/build/index.jsnode qa/mcp-smoke.mjs python 03-mcp/templates/python-server/server.py
# also call a tool with JSON argumentsMCP_SMOKE_CALL='search_notes {"query":"test"}' node qa/mcp-smoke.mjs node build/index.jsIt fails if:
- the server doesn’t answer
initializewithin the timeout - any stdout line isn’t valid JSON-RPC (the stdout pollution bug)
tools/listreturns no tools, or a tool without adescriptionorinputSchema- the optional tool call returns a protocol error
npm run qa:templates runs this against both templates. Add the same command to your own CI.
In-process alternative: in TypeScript, InMemoryTransport.createLinkedPair() (exported from @modelcontextprotocol/server) connects a client (from @modelcontextprotocol/client) and your McpServer in one process, which is ideal for Vitest or Jest. In Python v2, pass your MCPServer instance straight to mcp.Client(server) inside an async with block.
3. MCP Inspector
Section titled “3. MCP Inspector”npx @modelcontextprotocol/inspector node build/index.js # TypeScriptuv run mcp dev server.py # Python (MCPServer)npx @modelcontextprotocol/inspector # then enter a remote URLCheck each of these:
- Tools, Resources, and Prompts tabs list what you expect
- Each tool’s description and parameter descriptions read clearly
- The happy path works for every tool
- Every error path returns a helpful
isErrormessage - Large inputs are truncated or paginated sensibly
- The Notifications / stderr pane shows your logs, and nothing leaks to stdout
4. End-to-end evals
Section titled “4. End-to-end evals”In a real host, in a fresh chat each time:
| # | Prompt | Expected tool | Expected args | Result |
|---|---|---|---|---|
| E1 | “Find my notes about onboarding” | search_notes |
query: "onboarding" |
|
| E2 | “Save this as a note titled Standup” | add_note |
title: "Standup" |
|
| E3 | “What’s the weather?” (unrelated) | none | n/a | |
| E4 | “Save a note titled Standup” again | add_note → error → model explains |
If the model picks the wrong tool, fix the descriptions first, then the names, then consider merging tools.
Debugging playbook
Section titled “Debugging playbook”| Symptom | Where to look | Likely fix |
|---|---|---|
| Server won’t start | Host MCP logs; run the command manually in a terminal | Wrong path, a missing build, or a missing runtime on PATH |
| Starts, then disconnects | Run the smoke test; look for non-JSON on stdout | Move all logging to stderr |
| Tool list empty | Inspector | Tools registered after connect()? Register them before connecting |
| Model never calls the tool | Description | Add trigger words; say when to use it |
| Wrong arguments | Schema | Add description, enum, and format to each parameter |
| Timeouts | Server logs | Make I/O async, add timeouts upstream, report progress |
| Works in the Inspector, not in the host | Host env and cwd | GUI apps don’t inherit your shell PATH or env; set env and absolute paths explicitly |
Where the logs are
Section titled “Where the logs are”| Host | Logs |
|---|---|
| Cursor | Output panel → “MCP Logs” |
| Claude Code | claude --debug; /mcp in a session |
| Claude Desktop | %APPDATA%\Claude\logs\mcp*.log (Windows), ~/Library/Logs/Claude/ (macOS) |
| Your server | stderr (shown in the host logs) |