Your MCP server can pass every Inspector check and still lose the model on its first real request. This is the logging, contract-test, eval and tracing toolchain we use to catch that before users do.
Every MCP server we've been asked to rescue had a working tools/list. With the official TypeScript SDK or FastMCP, getting that far takes a morning. That's why most lists of MCP server development tools stop at the SDK. The failures users see come later, when a model reads forty tool descriptions and picks the wrong one.
A typical case: the server passes every manual call in the Inspector and gets wired into Claude. On the first real request, the model calls search_records instead of get_record and passes '03/04' where the schema expects ISO 8601. It gets back 'Internal error', then retries the same bad call twice. The protocol never failed. The tool contract did, and no protocol-level tool can see that. Here's the toolchain that catches it, in the order the failures show up.
npx @modelcontextprotocol/inspector is the first command we run on every project. It connects over stdio or Streamable HTTP, lists tools, resources and prompts, fires each tool with arguments you write by hand, and shows the raw JSON-RPC. Within ten minutes it catches the bugs that stop a server cold: an inputSchema that isn't valid JSON Schema, a capability you use but never declared, a handler that throws a protocol error instead of returning a result with isError: true. It also catches the server that hangs on initialize because a Postgres pool blocks on connect before the handshake finishes. Open the connection lazily instead.
What it can't tell you is whether a model can use any of it. You typed those arguments. You know IDs look like INV-2231, that status takes 'open' and not 'Open', and which of four search tools fits. The model gets a name, a description and a schema. A clean Inspector session proves the server speaks MCP. It says nothing about whether a model can use it.
Over stdio, stdout is the protocol channel. One print() or console.log() puts non-JSON in the middle of the stream. Some clients drop the connection with a parse error. Others skip the bad line, so the server works until some rarely used code path logs something. Dependencies do this too: dotenv 17 started printing an 'injecting env' line to stdout by default, which broke stdio servers on upgrade until they passed quiet: true. Send all logging to stderr from the first commit, and enforce it with ESLint's no-console (allowing only console.error) or ruff's T201 in Python, failing CI on either.
Then find the client's logs. Claude Desktop on macOS writes one file per server under ~/Library/Logs/Claude/, named mcp-server-<name>.log. It's often the only place a startup crash or a missing env var shows up. Most 'it won't connect' tickets we get turn out to be spawn ENOENT. The config says uv or node, which your shell finds through nvm or Homebrew, but the desktop app launches with a minimal PATH. Put absolute paths in the config.
The official SDKs ship a client next to the server. That lets a test start the server in-process and connect over an in-memory transport: InMemoryTransport.createLinkedPair() in TypeScript, or a FastMCP Client pointed at the server object in Python. We test through the protocol, not by calling handlers directly, because the bugs live in the layer in between. A Python Optional[str] becomes an anyOf with a null branch that some function-calling APIs reject. A datetime hint becomes a string whose format the model ignores. Calling the function yourself shows you none of this.
Each tool gets four cases: happy path, invalid arguments, not found, upstream failure. The error assertions matter most. A failing tool must return text the model can recover from: 'Invoice INV-2231 not found. IDs match INV-####. Call list_invoices to search.' A stack trace gets you the same bad call again. We also assert on response size. A tool that dumps every column of every row passes functional tests, then hits Claude Code's 25,000-token default cap on MCP output. Result size gets a test budget, just like latency.
Most teams skip this layer, and it's the one that finds the bugs users report. The harness is 30 to 100 tasks taken from real tickets and requests, run through a real model connected to your server, with the full tool-call trace captured. Grade the trace, not the answer. Did it pick the right tool first? Did the arguments validate on the first call? How many calls did the task take, and did the model stop once it had the answer? Run each task at least three times. Tool selection isn't deterministic, and passing one run in three counts as a failure.
The fixes are rarely code. They're renames, descriptions that say when not to use a tool, enums in place of free-text strings, and deleting the tool that overlaps two others. On one project, merging seven narrow lookup tools into two with an explicit filter parameter fixed most misrouted calls without changing a single query. Run the suite against at least two model families, because a description that routes cleanly on one can misroute on another. Treat a changed tool name or description as a code change: the suite runs in CI before it merges.
Evals cover the tasks you wrote down, and production covers the rest. Log every tool call with its name, argument keys and types, latency, result size in tokens and isError status, all tied to a session ID so you can replay the order of calls in a conversation. Redact the values and keep the structure.
We flag two patterns. The first is the same tool called three or more times in one session with slightly different arguments, which means the model is guessing at your schema. The second is a tool with zero calls in a week. Its definition costs context on every request and does nothing, so merge it or cut it. Both patterns become new eval cases that same week. That loop decides whether the server holds up far more than the SDK you started with.
A software studio that ships and maintains its own products — KeepChats, Gwora and Cairn — and builds the same way for clients. Founded and led by codewithumar.
Talk to the studio →Send the problem. You get one fixed number and a plan back within a business day.
We build software worth keeping — for clients, and for ourselves.
Founded & led by codewithumar