MCP Server and Client Best Practices
Every practice on this page traces back to a real failure mode covered elsewhere in this section: a tool that never showed up because it wasn't registered, a schema change that broke an existing client, a shared server with no rate limiting that one caller could starve.
Use this as a pre-flight check before moving a server from local development to anything shared or remote.
- Walk it top to bottom the first time you deploy a new server, tool/resource design first, testing last.
- Revisit sections C through E (transport, auth, versioning) any time you change how or where a server is deployed.
- Not every rule applies to every server, a purely local stdio tool has no real need for authentication or rate limiting.
- Treat an unchecked item as a known gap, not a blocker, but one you should be able to explain if asked.
Do I need to follow every rule on this checklist for a small local tool?
No. Sections A, B, and G apply to nearly every server regardless of size. Sections C through F are mostly about shared, remote deployment, a single-user stdio tool can reasonably skip authentication and rate limiting entirely.
What's the single most common mistake covered here?
Writing a tool handler and never registering it with the server's decorator. It passes any test that calls the function directly and is completely invisible to a real client, since it never appears in the capability manifest.
Why does authentication come before rate limiting in this checklist?
Rate limiting needs a resolved client identity to key its counters against. Without authentication in place first, there's no reliable way to distinguish one caller from another on a shared server.
Should I add rate limiting to a server before it has real usage?
Not necessarily. It's reasonable to defer rate limiting until a server has more than one real client, adding it prematurely is extra complexity with no immediate payoff for a single-user deployment.
How strict should schema versioning discipline be for an internal-only server?
Less strict than a public server, but not absent. Even internal clients break if an argument is renamed without warning, additive-only changes are cheap enough to be worth doing everywhere.
What's the fastest way to check that section B (registration) is actually satisfied?
Call list_tools() through a real client session and confirm every handler you've written shows up by name. This is exactly the integration test pattern covered in section G.
Is TLS really necessary for an HTTP/SSE server on a private network?
Yes, as a default. Private networks aren't always as isolated as assumed, and access requirements have a way of expanding to include external clients later, at which point missing TLS is a much bigger retrofit.
Do tools and resources need different testing approaches?
The same two-layer approach applies to both, unit test the underlying function directly, then confirm through an integration test that a real client can call the tool or read the resource and get the expected result.
What should I do if I find a tool argument that needs to be removed?
Follow section F: keep it working, mark it deprecated in the description with a removal window, and only remove it once that window has passed and known clients have had a chance to migrate.
Can this checklist be used as a code review gate before shipping a new server?
Yes, that's a reasonable use, walking sections A, B, and G during review for any new server, and C through F specifically when the deployment target is shared or remote.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the current Model Context Protocol Python/TypeScript SDKs. Model names, SDK versions, and the MCP spec move quickly - verify current specifics at platform.claude.com/docs and modelcontextprotocol.io before relying on them.