10 checks before you ship an MCP server
A distilled, genuinely useful pre-ship checklist: tool descriptions for LLM callers, actionable errors, untrusted-output injection, path traversal, rate limits, transport choice, versioning, and testing.
Why a checklist, not a framework
Most of what makes an MCP server good or bad isn't enforced by the protocol at all — MCP will happily let you ship a server with vague tool descriptions, unbounded outputs, and a database role with write access to tables no tool needs. The protocol gives you the shape (tools, resources, prompts, JSON-RPC over stdio or HTTP); the quality bar is a set of habits layered on top of that shape. The ten checks below are the ones that catch real, recurring failure modes — not a restatement of the spec.
The checklist
- 01
Tool descriptions are written for the model, not for you
The reader of a tool's description field is an LLM deciding, from natural-language text alone, whether this tool is the right one for the current request. A description that reads like an internal code comment ("fetches records") gives the model nothing to disambiguate on. State what the tool does, when to use it versus its nearest neighbor, and any non-obvious side effect (does it write, does it cost money, is it irreversible) in the description itself — not buried in a README the model never sees.
- 02
Errors name the problem and the fix
A generic "400 Bad Request" or a bare stack trace gives a model nothing to act on except retrying the same broken call. A good tool error names the specific bad input, states why it's invalid, and — where possible — suggests the corrective action: a different tool, a corrected parameter shape, a narrower query. The bar is whether the model can recover without a human reading the error for it.
- 03
Untrusted tool output is treated as untrusted
Anything a tool returns that your server didn't generate itself — a fetched web page, a file's contents, a third-party API response — can contain text crafted to look like instructions to the model reading it. Wrap or label content your server didn't author, strip or neutralize anything that resembles a system-prompt-style directive, and never let a tool's job be "fetch arbitrary external content and hand it to the model unmarked."
- 04
Path-taking tools resolve, then boundary-check
Never trust a caller-supplied path by string comparison alone (rejecting strings that merely contain ".." is not enough — symlinks, encoded traversal, and absolute paths all get around a naive check). Resolve the path to its canonical absolute form first, then confirm the resolved path is still inside the allowed root before touching the filesystem.
- 05
Real cost or real side effects are rate limited server-side
A tool that calls a paid API, sends an email, or triggers a deploy needs a rate limit enforced inside the server, not just documented in the tool description. A model can call a tool far faster than a human would, and a runaway loop (a retry storm, a misreading of the task) will find any limit that exists only as a suggestion.
- 06
Transport matches the actual deployment shape
stdio suits a single local client spawning your server as a subprocess — simplest to build, no auth surface to worry about because the process boundary is the trust boundary. HTTP/SSE suits a server reachable by multiple remote clients, and brings a real authentication and authorization question with it. Pick based on today's actual deployment, not a speculative future multi-tenant need — the added complexity of HTTP isn't free, and stdio-to-HTTP is a mechanical migration when you actually need it.
- 07
Tool-contract changes carry a version signal
Renaming a tool, changing which parameters are required, or changing what a return value means is a breaking change for every client already integrated against the old contract — even though nothing about MCP itself will stop you from shipping it silently. Bump a semver-major version and say so explicitly in a changelog, separate from internal refactors that don't touch the contract.
- 08
The server has been driven by something other than your own client
A server that only your hand-built test client has ever talked to has an easy path to hidden assumptions about call order or argument shape. At minimum, drive it with a small dependency-free script that speaks raw JSON-RPC over stdio — initialize, list tools, call each one with both valid and deliberately invalid arguments — before trusting it against a real client.
- 09
Credentials are scoped to what the server's tools actually need
An MCP server that talks to a database or a cloud API should authenticate with a role scoped to exactly the operations its tools perform — read-only where the tools are read-only, no admin scope because it was convenient during development. If a single compromised or manipulated tool call shouldn't be able to do more damage than that one tool is supposed to do, the credential has to enforce that, not just the application logic.
- 10
Docs answer "what does this expose" for a human, not just a model
Someone deciding whether to install and run your server needs a plain-language answer to what it can read, what it can write, what external services it calls, and what credentials it needs — before they grant it any of that. Tool descriptions serve the model at call time; a README serves the human deciding whether to grant access at all. Both are required, and they answer different questions.
How to use it
Run it once before a server's first ship, and again — in full, not just eyeballing the diff — after any change that touches a tool's description, parameters, or return shape. A broadened description that seemed like a harmless clarification can introduce new false-positive routing on requests that previously reached a different tool correctly; that only shows up if you actually re-test with realistic phrasing, not by reading the diff.
This guide covers the concept; the MCP Server Starter Kit packages the full implementation — two complete runnable reference servers (TypeScript and Python) built against every item above, a dependency-free RPC probe script for layer-one testing, and the longer-form design and security guides these checklist items are distilled from.