DUSKELStart a project
7 min read

Building an MCP server: treat your tools like a contract

MCP server development is less about the protocol and more about tool design. Here's how to build tools an agent can actually call without guessing.

The Model Context Protocol is a standard way to hand tools, data, and prompts to a language model. Instead of writing a bespoke integration every time you want an agent to touch your database, your CRM, or your internal API, you expose an MCP server, and any MCP-aware client — a coding agent, a chat client, a workflow runner — can discover and call it. That is the pitch, and it is a good one.

But the protocol is the easy part. The SDKs handle transport and message framing for you. The hard part, the part that decides whether your server is useful or a liability, is tool design. An MCP tool is a contract you are offering to a non-deterministic caller that will read your description, make an assumption, and invoke you. Design it like a contract and MCP server development goes smoothly. Design it like an internal function and the agent will misuse it in ways you didn't imagine.

Why MCP matters now

Before MCP, every agent-to-system connection was custom glue. Ten tools across three agents meant thirty integrations, each with its own auth, schema, and failure modes. MCP collapses that into one interface: build the server once, and every compliant client speaks to it. The ecosystem effect is the point — the same server that powers your internal coding agent can power a customer-facing assistant without a rewrite.

It matters now specifically because agents got good enough to be trusted with real actions, which means the surface area of what they can break also grew. A standard interface is only an asset if the tools behind it are scoped, described, and defended properly. Otherwise you have just standardized the way things go wrong.

Design tools for the caller, not for your codebase

The single most important field in a tool definition is the description. The model chooses which tool to call, and with what arguments, based almost entirely on the name and description. Vague descriptions produce wrong calls. 'Gets user data' is useless — data by what key, returning what, when should the agent reach for this versus another tool? Write the description as if onboarding a new engineer who will never get to ask you a follow-up question.

Keep tools coarse and task-shaped, not thin wrappers around individual endpoints. An agent orchestrating eight granular calls to accomplish one obvious task is eight chances to derail. A single 'create invoice and email it to the customer' tool that does the orchestration internally is far more reliable. Make the input schema strict and typed, with enums instead of free-form strings wherever the set of valid values is known, and mark required fields as required. Every constraint you encode in the schema is a mistake the agent cannot make.

Scope permissions like you mean it

An MCP tool runs with whatever authority you give it, invoked by a caller that can be talked into things through the content it reads. Treat every tool call as untrusted input. Scope credentials to the narrowest set of operations the tool genuinely needs — a tool that reads orders should not hold a token that can delete them.

Separate read tools from write tools, and make destructive actions loud: require explicit confirmation parameters, and consider keeping anything irreversible behind a human approval step rather than exposing it to the agent at all. Validate and authorize inside the tool on every call. The client is not your security boundary; your server is. Assume the arguments are adversarial and check them accordingly.

Errors are messages to the model, not stack traces

When a tool fails, the error goes back to the model, which will try to recover based on what you tell it. So write errors for that reader. 'Error 500' teaches the agent nothing and it will retry the same failing call. 'The date must be in YYYY-MM-DD format; you sent 07/04/2026' tells the model exactly how to fix its next attempt, and it usually will.

Distinguish the failure types clearly: bad input the agent can correct, a missing resource, a permission denial it should stop retrying, and a transient error worth one retry. Never leak internal stack traces, secrets, or connection strings into an error string — that text lands in the model's context and possibly in front of a user. Return structured, actionable, sanitized errors, and your tools become self-correcting instead of self-repeating.

Test the way an agent actually calls

Unit-testing your tool functions is necessary but not sufficient. The real test is behavioral: give a model the tool and a realistic task and watch which tool it picks, what arguments it constructs, and how it responds to your errors. That is where you discover that two tools have overlapping descriptions and the agent keeps choosing the wrong one, or that your error messages send it into a loop. Fix the descriptions and schemas until the agent gets it right without hand-holding. That feedback loop is the whole job of MCP server development.

Want an MCP server your agents can actually trust? We build them.