MCP Server Design: Fewer Tools, Sharper Boundaries
Most MCP servers are a thin wrapper around an existing API, which is why agents use them badly. Design rules for servers models can actually operate.
The usual way an MCP server gets built is to take an existing REST API and expose one tool per endpoint. Forty endpoints become forty tools, the server ships, and agents using it perform noticeably worse than they do with three hand-written tools.
The reason is that an MCP server is not an API surface. It is a prompt. Every tool name, description and parameter goes into the model's context and competes for attention with the actual task. Designing one is much closer to writing documentation for a distracted reader than to designing an interface for a program.
Surface area is the primary cost
Each exposed tool consumes context before any work happens, and more importantly it adds a branch to a decision the model has to get right on every turn. Forty similar tools means forty chances to pick the wrong one.
The right target is usually well under a dozen tools per server. Get there by collapsing variants rather than by hiding functionality: one search_issues with a filter object beats search_issues_by_author, search_issues_by_label and search_issues_by_date.
Collapse in the other direction too. If two tools are almost always called in sequence — create a record then attach a file to it — consider a single tool that does both. Every round trip you remove is a turn saved, and turns are the unit that agent cost and latency scale with.
Be willing to omit things. An endpoint that exists in your API because some integration needed it in 2019 does not need to be in the agent's decision space. Tool schema design covers the per-tool detail; this is about which tools exist at all.
Use resources for what the application should choose
MCP separates tools, which the model invokes, from resources, which the host application reads and places into context. The distinction is about who decides, and getting it wrong is the most common structural mistake in server design.
If the selection depends on the task and requires judgement, it is a tool. If the material is simply needed and a human or the host can pick it, it is a resource. Exposing a read_document tool for a document that should have been offered as a resource costs a full turn and a decision the model did not need to make.
Prompts, the third primitive, are for workflows you want a human to trigger deliberately. They surface as commands in the host, which makes them a good home for the multi-step operations you do not want an agent initiating on its own.
Design the return value before the parameters
What a tool returns becomes prompt content, so a tool that returns your API response verbatim is dumping pagination metadata, internal identifiers, null fields and audit timestamps into the working context.
Return the smallest thing that lets the agent decide what to do next. A search should return matches with enough identity to fetch the full record, not the full records. A list should be capped and should say how many were omitted, so the model knows to narrow the query rather than assuming it saw everything.
Prefer stable human-readable identifiers over opaque ones where you have the choice. A model that sees a file path can reason about it; a model that sees a UUID can only copy it, and it will occasionally copy it wrong.
Include units, and include them in the value rather than only in the schema. A field that reads "duration": "1500ms" survives being quoted back in a summary in a way that a bare number does not.
Errors are the highest-leverage strings you write
A tool error is not a failure signal to a program, it is an instruction to a reader who will immediately try again. Treat every error message as a chance to make the next attempt succeed.
Say what was wrong and what to do instead. "Invalid date format" is a dead end; "date must be ISO 8601, for example 2026-08-30, received 30/08/2026" gets fixed on the next call. If a required parameter was missing, name it and describe it. If the record does not exist, say whether a search tool would help find it.
Distinguish retryable from terminal errors explicitly. A rate limit should say so and say roughly how long to wait; a permission denial should say plainly that retrying will not help, or the agent will burn its turn budget trying. Agent error recovery patterns covers the loop side of this contract.
Statelessness, sessions and idempotency
Agents retry. Connections drop, turns get replayed, and a speculative execution layer may run a call the model has not formally issued yet. A server whose write operations are not idempotent will eventually create three tickets for one request.
Accept an idempotency key on every mutating tool and deduplicate on it. This costs little and removes a whole class of incident that is otherwise very hard to reproduce.
Keep as little session state as you can. Stateless servers are trivially horizontally scalable and survive reconnection; stateful ones need affinity and a story for what happens when the client comes back. If you do hold state, make it recoverable from the client side rather than required. MCP transport options covers how session identity works over each transport.
Versioning without breaking every agent
Changing a tool description changes model behaviour, which makes every edit a behavioural change rather than a cosmetic one. Treat descriptions as production configuration, with review and a rollback path.
Prefer additive changes. Adding an optional parameter is safe; renaming one silently breaks callers whose prompts referenced the old name in an example. If you must break something, add the new tool alongside the old, mark the old one deprecated in its description with a pointer to the replacement, and remove it after a real deprecation window.
Run a small evaluation suite against your own server the way you would against a model, because a description change that reads better to you can measurably reduce tool selection accuracy. Agent regression suites covers making that a gate rather than a hope.
A checklist before you publish
Under a dozen tools, with names that are distinguishable at a glance. Descriptions that say when not to use the tool, not only what it does. Resources for material the host should select. Capped, trimmed return values that name what was omitted. Errors that state the fix. Idempotency keys on writes. Additive versioning with a deprecation path.
Then test it with an agent rather than with a script. Give a model a realistic task and read the trace. Every wrong tool choice and every malformed argument is a documentation bug in your server, not a model failure — and it is the fastest feedback you will get. Building an MCP server walks through a working implementation.
Common questions
How many tools should one MCP server expose?
Usually fewer than a dozen. Every tool competes for context and adds a branch to a decision the model must get right each turn, so collapse variants into one tool with a filter object rather than shipping one tool per endpoint.
When should something be a resource instead of a tool?
When the host application or a human can select it without judgement. Tools are for choices that depend on the task. A read tool for material that should have been offered as a resource wastes a turn and a decision.
Why do MCP tools need idempotency keys?
Because agents retry. Dropped connections, replayed turns and speculative execution all mean a mutating call can arrive twice. Deduplicating on a client-supplied key removes a class of bug that is very hard to reproduce.