What an MCP server looks like in production
· 4 min read · mcp, ai, norbelys
Norbelys ships an MCP server. An agent connects to it and can run the whole product: build an audience, write a sequence, check deliverability, launch, read the analytics. It exposes roughly eighty-two tools.
Getting the protocol working took an afternoon. Everything since has been about the parts the protocol does not cover.
Eighty-two tools is a design problem, not a size problem
The instinct is to think eighty-two is too many, and to consolidate — one campaign tool with a
big action parameter instead of fourteen specific ones.
That is worse. A model choosing between fourteen well-named tools with narrow schemas makes far fewer mistakes than one choosing an enum value inside a general tool, because the tool name is itself the strongest signal in the decision. Collapsing tools moves the ambiguity from a place the runtime can validate into a place only your handler can.
What actually matters at eighty-two is that no single task requires the model to hold all of them in mind. Tools group by resource, the descriptions say what the thing is for rather than what it does mechanically, and the ones that matter for a given job are reachable in one step from the one before.
Annotations, and the asymmetry nobody mentions
An MCP tool can carry hints about how it behaves: readOnlyHint, destructiveHint,
idempotentHint, openWorldHint. They shipped in the 2025-03-26 revision of the spec. A client
reads them and decides whether to auto-approve a call or stop and ask.
It is tempting to call this the security model. It is not, and the
spec says so plainly:
annotations are not guaranteed to describe behaviour faithfully, and clients must treat them as
untrusted unless the server is trusted. An untrusted server can declare readOnlyHint: true and
delete your files anyway.
So here is the asymmetry. From the client side, annotations are a claim you are allowed to disbelieve. From the server side — my side — they are a promise, and the only thing that makes the product usable by an agent at all.
Because the alternative is worse than useless. A tool that sends 100,000 emails and a tool that counts them must not look the same to the client. If they do, the only safe policy is to confirm everything, and an agent that asks permission eighty-two times is slower than doing it by hand.
That is the whole difference between “the agent can read my campaigns” and “the agent can run my campaigns”, and that difference is the product.
Most servers do not bother. Annotation adoption is uneven, clients vary in how strictly they honour the pessimistic defaults, and the closest production analogue — GitHub’s read-only mode — reaches around 17% of users. Declaring them correctly is cheap, and almost nobody does it, which is a strange place for a competitive advantage to be sitting.
The send door
There is one place in the system where sending actually happens, and it is a door with a lock on it.
Everything else — audience building, writing, scheduling, previewing — is reversible. Sending is not. So sending is a single gated action with its own checks, and no other tool can reach past it, including the agent’s.
This is not an AI-specific principle, but agents make it urgent. A human who does something irreversible by accident does it once. A loop does it as fast as your rate limiter allows.
Documentation is part of the surface
An MCP server that a model can technically call is not the same as one it can call correctly.
So the agent kit is published separately, as plain Markdown: how the API is authenticated, the audience filter grammar, the campaign structure, the deliverability rules, worked examples. It installs into whatever directory the assistant reads skills from and works across Claude Code, Cursor, Codex, Copilot and the rest.
It contains no source code and no secrets. It is documentation, deliberately — the thing the model needs is not access to the implementation, it is an accurate account of the interface and the rules around it.
What I would tell someone starting
Build the tool list from the jobs, not from the database tables. Declare the annotations honestly before you chase coverage — not because a client is obliged to trust them, but because a client that cannot trust them has to confirm everything. Put a lock on anything irreversible and route every path through it. And write the documentation as if the reader is a competent stranger with no context, because that is exactly what a model is.