# I've built a handful of MCP servers. Here's what separates a good one from a demo.

> Source: <https://dev.to/freema/ive-built-a-handful-of-mcp-servers-heres-what-separates-a-good-one-from-a-demo-4i4f>
> Published: 2026-07-28 13:00:00+00:00

This year I've built more MCP servers than I meant to, across three pretty different shapes:

Personal, product, enterprise. Totally different data, totally different stakes. And yet the things that made each one *good*, versus a flashy demo that falls over the moment a real agent uses it in anger, were the same every time. Here are the seven that keep recurring.

This is the one nobody warns you about. Every tool definition (name, description, JSON schema) gets loaded into the model's context on *every single request*, whether or not it's ever called. Twenty tools of boilerplate and the agent is reading pages of plumbing before it touches your data.

So the design pressure runs the opposite way from a normal API: fewer, broader tools beat many narrow ones. vellum has 15 core tools and I fought to keep it there. If a tool doesn't earn its slot in the context window on most requests, it shouldn't be a tool.

The corollary to #1. Most servers expose everything (including "read this thing") as a tool. But MCP has a better primitive for reading: **resources**. In vellum, every note is a resource at a stable URI (`vellum://note/projects/x.md`

). The agent attaches a document by reference instead of spending a tool round-trip to fetch it, and it can *subscribe* to that URI and get told the moment the note changes.

Reading through a resource costs no tool definition. Reading through a tool costs one on every request. Use the right primitive.

A token with no grants should see **zero tools**: not an error when it tries to call one, but genuinely nothing in `tools/list`

. Authorization fails *closed*. Rate limiters, on the other hand, should fail *open*: if the limiter itself breaks, you degrade to serving the request, not to locking everyone out. Getting these two backwards is how you either leak capability or take yourself down.

MCP lets you tag tools with hints: `readOnlyHint`

, `destructiveHint`

, `idempotentHint`

. Use them. They're how a well-behaved client knows it can call your search tool freely but should think twice before your delete tool. On the marketing server this is the line between "the agent can answer any question about your analytics" and "the agent can reconfigure your analytics", and the annotations are what keep those two worlds visibly separate.

The moment more than one thing can write (an agent *and* a human, or two agents) last-write-wins silently eats data. vellum returns a content hash on every read, and a write fails on hash mismatch instead of clobbering. It's optimistic concurrency, the same trick databases have used forever, and it turns "the agent overwrote my edit" from a support ticket into a retry.

The enterprise gateway taught me this one hard. When an MCP server sits in front of a company's actual user data, the default posture is read-only, every tool call is logged with who called it and what came back, and any PII in a response comes back as a hash, not the raw value. An agent is a new kind of caller; treat it like one you'll have to explain to a security review later, because you will.

MCP has a spot in the handshake for **server instructions**: a short document the agent reads before it does anything. This is where you say "notes are vault-relative paths," "prefer patch over rewrite," "empty query with tags is a pure tag filter." It's the difference between an agent that fumbles your API and one that uses it the way you intended on the first try. Most servers leave it blank. Don't.

`structuredContent`

roughly *doubles* your response size: the payload is serialized into the response twice. If you've got size-based metrics or limits, they'll suddenly read high for no obvious reason. Recalibrate them, or you'll spend an afternoon chasing a leak that isn't there.

None of this is about clever features. Across a note vault, a marketing stack and an enterprise gateway, the servers that behaved well were the ones that respected the agent's context, used the right primitive for each job, and were honest about what was safe to call. The demo is easy. The seven things above are what make it survive contact with a real agent.

*If you're building on MCP: which of these bit you first? For me it was #1. I over-exposed tools on my first server and watched the agent drown in its own toolbox.*
