Skip to content

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

Keep handler logic in plain functions and test them directly:

// src/notes.ts exports searchNotes(notes, query, limit); the tool handler just calls it
import { searchNotes } from "../src/notes.js";
test("search is case-insensitive", () => {
expect(searchNotes([{ title: "MCP", body: "", created: "" }], "mcp", 5)).toHaveLength(1);
});

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:

Terminal window
node qa/mcp-smoke.mjs <command> [args...]
node qa/mcp-smoke.mjs node 03-mcp/templates/typescript-server/build/index.js
node qa/mcp-smoke.mjs python 03-mcp/templates/python-server/server.py
# also call a tool with JSON arguments
MCP_SMOKE_CALL='search_notes {"query":"test"}' node qa/mcp-smoke.mjs node build/index.js

It fails if:

  • the server doesn’t answer initialize within the timeout
  • any stdout line isn’t valid JSON-RPC (the stdout pollution bug)
  • tools/list returns no tools, or a tool without a description or inputSchema
  • 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.

Terminal window
npx @modelcontextprotocol/inspector node build/index.js # TypeScript
uv run mcp dev server.py # Python (MCPServer)
npx @modelcontextprotocol/inspector # then enter a remote URL

Check 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 isError message
  • Large inputs are truncated or paginated sensibly
  • The Notifications / stderr pane shows your logs, and nothing leaks to stdout

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.

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
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)