Skip to content
AI ENGINEERING

How to Build an MCP Server for Your SaaS Without Shipping a Security Hole

The protocol is a day of work. Everything after it is the job. What I learned running MCP servers for my portfolio and for Threadovo, from tool descriptions as prompt input to the auth model that quietly got rewritten.

7 min readBy Daniel Olawoyinmcp server · model context protocol · ai agents

Three people asked me this month whether their product needs an MCP server. Two had already started building one. All three assumed the protocol was the difficult bit.

It is not. If you want to build an MCP server for your SaaS, the transport and the handshake are about a day of work, less if you use the official SDK. What costs you is everything after that, because you have handed a language model a row of buttons inside your product and nobody is standing behind it watching which ones it presses.

I run one for this site. It reads my blog inventory, audits pages for SEO, drafts posts, and pulls leads out of the contact form. I built a second one for Threadovo so its planning agents can query and write marketing content without me gluing endpoints together by hand. Neither of them looks much like the version I first designed on paper.

An MCP server is an API with a much worse caller

Your REST API has a caller who read the docs, wrote code once, and will send roughly the same shaped request forever. If they misuse an endpoint you get a reproducible bug report.

An MCP server has a caller that re-reads the docs on every turn, decides what to do based on whatever text happens to be sitting in its context window, and can be talked out of its own judgement by a sentence buried in a lead message it was asked to summarise. Most of what I knew about API design survived that change. A decent amount did not.

Destructive operations are where the difference shows up first. On a normal API a delete endpoint is unremarkable, because a human wrote the call site and that human is accountable. On an MCP server the model decides when to fire it, and its reasoning is shaped by content you do not control. My portfolio server has no delete tool. I keep meaning to add one for cleaning up test posts and I keep deciding it is not worth it.

Your tool descriptions are model input, not documentation

This is the flip that took me longest to internalise. The description field on a tool is not a comment for whoever reads your source next. It lands in the model's context on every request, and the model reads it as instruction.

So write it like a prompt. What the tool does, when to reach for it, when not to, what the limits are. My create-post tool spells out in its description that status defaults to draft and that publishing needs an explicit ask from the user. That sentence has prevented more accidental publishing than any validation I wrote, because the model reads it before it ever makes the call.

Which cuts both ways. Anyone who can influence a tool description can influence the model, and that is the whole basis of the tool poisoning attacks people spent this year cataloguing: a malicious or quietly mutated description carrying instructions to the agent. If you pull tool definitions from a registry, or let tenants register their own servers, you have given strangers a writable prompt inside your product. Microsoft's survey of MCP security in 2026 walks through how that plays out.

I wrote the general version of this argument in prompt injection is a permissions problem, and MCP has not changed my mind about it. It has mostly just given the attacker a tidier interface to work with.

Tool count is a budget, and most teams blow it

Every tool you expose costs context. Call it a few hundred tokens each once descriptions and schemas are counted. Twenty tools is comfortable. A hundred across five servers is tens of thousands of tokens spent before the user has typed anything, and selection accuracy starts slipping long before you run out of window.

Cloudflare published a number on this that stuck with me: their full native tool surface came to over a million tokens of definitions.

My portfolio server exposes thirteen tools and I have twice sat down to cut it to nine. The candidates are not broken tools. They are tools close enough to a neighbour that the model occasionally picks the wrong one, and a tool that gets picked wrongly is worse than a tool that does not exist. I have not made the cut yet, mostly because I use all thirteen myself and I am not sure my usage is a good guide to the agent's.

What I would do differently from the start is design around jobs instead of endpoints. Six well named tools will outperform thirty granular ones. Put related operations behind a single tool with a mode parameter instead of shipping a tool per verb. And do not mirror your database schema, because a schema describes your storage, not anyone's work.

Auth got rewritten and half the ecosystem has not noticed

The early spec had MCP servers acting as their own authorization server, minting their own tokens. That approach is gone. The current model treats your server as an OAuth 2.0 resource server with audience bound tokens and PKCE, and client registration has moved off dynamic registration. Read the current specification rather than any blog post from last year, including this one. The ground keeps shifting.

Two things matter more than spec compliance.

The token your server receives has to be scoped to the user, not to your server. If your MCP server holds one admin credential and acts for whoever asks, you have built a confused deputy: every action in the audit log carries your service account's name, which makes the log decorative. In a multi tenant product the boundary has to sit below the tool layer, where the rest of your app enforces it. Building Scrivane is what convinced me isolation belongs in the data access layer instead of in each caller, and an MCP server is simply one more caller that has to obey it.

The second thing is scope. Asking for broad access because mapping real permissions is tedious is the most common mistake I see, and it is always framed as temporary. The agent will eventually use everything you gave it.

Errors are prompts too

Most API error handling is written for a log aggregator. A 400 with a validation blob attached is perfectly good for a human debugging at 2am.

An agent reads that string and picks its next move from it. Tell it the input was invalid and it will retry the same call with slightly different noise, burn tokens, then either give up or report something untrue to the user. Tell it the SEO title has to be 42 characters or fewer and that it sent 51, and it edits the field and carries on.

I went through my portfolio server and rewrote every error to say what was wrong and what a good value looks like. The retry loops mostly stopped. It is the cheapest improvement on this list by a distance, and I almost skipped it, because error copy reads like polish until you watch an agent trip over it twenty times.

Slow tools deserve the same treatment. If something takes ninety seconds, say so in the description and give the agent something to poll, or you get a timeout the model reads as failure and retries, which is how duplicate writes happen. I went through that in more detail in what breaks in long-running agent workflows.

How I would build an MCP server today

Start read only and ship it. Live with the thing for a fortnight and watch which tools the model reaches for and which it never touches, because that list will not match what you predicted. Then add writes one at a time, each with a small blast radius and a default that fails safe.

Going straight to a full CRUD surface is how teams end up with a server that is technically complete and practically unusable. I did a version of that myself on the first build and spent longer unpicking it than the original work took.

If a vendor tells you their MCP integration took an afternoon, ask them what happens when a tool description lies to the model. You will learn quickly whether they have run one in production or only demoed one.

mcp servermodel context protocolai agentsai engineeringproduction aiapi designoauth
Written by
Daniel Olawoyin

Full-stack & AI engineer based in Lagos. I build production systems with AI in them — voice agents, RAG pipelines, multi-tenant SaaS.