In short
To build an MCP server that works in production, give it one outside system to own, expose a small set of clearly named tools over JSON-RPC 2.0, choose stdio for local use and Streamable HTTP for remote, split every irreversible action into a draft step and a confirm step, and return a named error instead of silence when a call fails.
  • One system each: Byline publishes articles, MailMan handles Gmail with 29 tools, WhatsAppMan sends WhatsApp with 12. Three servers, three systems, no overlap.
  • Two transports only: the Model Context Protocol architecture documentation names stdio for local processes and Streamable HTTP for remote ones, the latter over HTTP POST with optional Server-Sent Events.
  • Two-step by default: MailMan and WhatsAppMan both run draft, then preview, then confirm. WhatsAppMan ships no raw send tool at all, so an assistant cannot send an unpreviewed message.
  • Version 2026-07-28: that is the currently documented protocol version, and every request carries the version plus the capabilities relevant to that request.

It is eleven at night, the deploy is finished, and you are pasting the same three commands into a terminal for the fourth time this week. In another window an assistant sits waiting, fully capable of running them, and completely unable to reach them. That gap is why most people decide to build an MCP server in the first place.

I want to talk about what happens after that decision. The wire format is settled and documented already. The design choices are not, and they are what decide whether your server is still installed six months later.

The protocol stopped being the hard part

According to the Model Context Protocol documentation, MCP is "an open-source standard for connecting AI applications to external systems", and the site offers an analogy I have not managed to improve on: think of MCP like a USB-C port for AI applications. One port, many devices, no custom cable for every pair.

The participants are easy to hold in your head. An MCP server is a program that provides context to MCP clients. The host is the AI application that coordinates one or many clients, and the documentation names Claude Code, Claude Desktop and Visual Studio Code as examples. The host creates one client per server. Your server therefore never needs to know who else is in the room.

Underneath sit two layers. The data layer speaks JSON-RPC 2.0 and carries discovery, primitives and notifications. The transport layer moves those messages, over stdio when the server runs as a local process and over Streamable HTTP when it runs remotely. That same documentation describes the protocol as stateless: every request carries the protocol version and the capabilities relevant to that request.

Three primitives are worth memorising. Tools are executable functions the application can invoke to perform actions. Resources are data sources that provide contextual information. Prompts are reusable templates. Most first servers need only the first one.

So the format is public, and the reference servers repository is there to read. What is left is judgement, and judgement is where first servers go wrong. If you are still deciding whether a server is even the right answer, I covered the whole family in how MCP servers, skills and plugins actually work together. This piece assumes you have decided.

What makes an MCP server survive production?

Four decisions carry the whole thing. The server owns exactly one outside system. Its tool list stays short, and a stranger reads every name correctly on the first try. Every tool carries a JSON schema the host can trust. No single call does anything irreversible. The rest is detail.

Start with the boundary. We have shipped three MCP servers at IndiaNIC, and every one of them owns a single system. Byline publishes articles. MailMan handles Gmail. WhatsAppMan sends WhatsApp. None of them knows the others exist.

Fair: one server doing all three would mean one process to install and one configuration block instead of three. That is true, and it is the reason most teams start there. It also means a single bad dependency takes down your email and your publishing together, and it means the host is handed forty tools where it needed four. Small servers are cheaper to reason about for the model, not only for you.

Then name the tools the way you would name a public API, because a tool list is a public API. The host reads your names and schemas and decides what to call, with nothing else to go on. MailMan exposes 29 tools, among them draft_email, confirm_send, draft_campaign, confirm_campaign, read_message, search_messages and schedule_send. Read that list once more. You can tell what each one does without opening a file, and so can the model. A host that keeps picking the wrong tool is usually telling you two of your names are too close together.

The three servers were built months apart, by different people, for different systems. The shape they converged on is the actual lesson.

ServerThe one system it ownsTool surfaceHow it refuses to be dangerous
BylinePublishing to Ghost, WordPress and LinkedIn feed postsBrief, score, image, publish, plus export handoffs for Medium and SubstackThree blocking checks on platform HTML, structure and the summary block; every tool returns a result or an error naming the API and its HTTP status
MailManGmail, over an SMTP and IMAP app password or an OAuth2 browser sign-in29 tools, paired as draft and confirmNothing sends until you approve; about 20 messages per minute; aborts a run near a 25% failure rate; resume prevents duplicate sends on retry
WhatsAppManWhatsApp, as a linked device through the Baileys library12 tools, send-only by designNo raw send tool exists; 100-recipient cap on bulk sends; jittered delays; circuit breaker after 3 consecutive failures

Look down the last column. Not one of those protections lives in the model. They live in the server, because the server is the only participant that cannot be talked out of them.

A tool that can be called in one step will eventually be called by accident.
A notepad with a hand-written numbered list open beside a laptop and a phone at a developer's desk late at night
The tool list comes before the code, and it usually fits on one page.

How do I build an MCP server, step by step?

Pick one outside system, list the actions worth exposing, name each action as a tool with a JSON schema, answer tools/list so the host can discover them, implement tools/call to run them, split anything irreversible into a draft tool and a confirm tool, return a named error on every failure, and test through the real tool layer rather than around it.

  1. Write the tool list before you write code. Plain names, one line each, on paper. If you cannot describe a tool in one line, it is doing two jobs.
  2. Give every tool a JSON schema for its inputs. The host has nothing but the name and the schema when it decides whether to call you, so an optional field with no description is a guess waiting to happen.
  3. Choose the transport from where the server runs, not from fashion. A local process on a developer machine wants stdio. A server other people reach over a network wants Streamable HTTP, and the MCP documentation recommends OAuth for obtaining authentication tokens on that path.
  4. Answer discovery honestly. The host calls tools/list to learn what you offer, and that response is your entire interface. List only what you will actually support next month.
  5. Implement tools/call as the single execution path. Validate inputs there, do the work, and return a result the host can read back to a human.
  6. Split the dangerous half. Anything that leaves your machine, charges money, or cannot be undone gets a draft tool and a separate confirm tool, with a preview in between.
  7. Make failure loud. Name the API you called and the status it returned. A tool that swallows a 403 and returns an empty list has taught the model that nothing went wrong.
  8. Test by calling your own tools through a host. Unit tests prove the code does what you told it; only the real tool layer shows you whether you told it the right thing.

There is no raw send tool in WhatsAppMan. An assistant can draft a message, show it to you, and send it after you say yes, and that is the only road through the server. I would defend that decision in any room. The cost is real, because a genuinely batch workflow now needs a human present, and we accepted the cost rather than arguing about it every quarter.

MailMan made me pay for my own rule once. It refuses to draft a campaign when the recipient list is missing the name placeholders the template requires, and on a day when I was in a hurry that refusal stopped me cold. I fixed the spreadsheet. I have never removed the check, because the alternative is an email that greets three hundred people by nothing at all.

Keep secrets out of the config file. A host's server configuration is ordinary JSON that gets committed, screenshotted and pasted into chat threads, so read tokens from the environment or from a credential store the server owns. Byline, MailMan and WhatsAppMan all run their own setup step for exactly this reason.

The bar I hold our own servers to

A finished MCP server is one a colleague installs without reading your source, uses correctly on the first attempt, and never has to ask "did that work?" about. That sentence contains three tests, and I apply all three before anything of ours goes out as a public repository.

The rule we wrote into Byline and never made an exception to is this: nothing fails silently, and every tool returns a result or an error naming the API and its HTTP status. It sounds like hygiene. It is actually the difference between an assistant that reports a publishing failure and one that cheerfully tells you the article is live when the platform quietly discarded half of it.

Byline also keeps a mechanical grader for its own output, with advisory checks on things like em dash density and repeated sentence openers, and three checks that block publication outright. Where those checks live matters more than what they measure. The server carries the standard, so the standard survives whoever is holding the keyboard that day.

On effort, here is my estimate from daily use rather than any measurement: a first server with three or four tools is a weekend, and the second week goes almost entirely into schemas and error messages. That ratio surprised me the first time. It has held for all three.

One more boundary is worth naming, because it saves whole weekends. If what you want to automate is a procedure rather than a system, an MCP server is the wrong shape, and a skill or a plugin is the right one. I worked through that choice in skills versus plugins versus MCP servers, and the short version is that servers exist for systems with state and credentials, while skills exist for knowledge you keep repeating.

Here is your twenty-four hour challenge. Open a terminal, write down the three commands you typed most often this week, and turn the safest one into a single tool on a stdio server that does nothing else. Not five tools. One. If it answers tools/list and returns a real error when you break it on purpose, you have built an MCP server, and the remaining work in 2026 is only addition.

Frequently asked questions

What language should I use to build an MCP server?

Use the language your target system already has a good client library in, because that library is most of the work. MCP itself is transport and JSON-RPC 2.0, so any language that can read stdin and write stdout qualifies. Our three servers are Node packages, installed globally from npm.

Should my first MCP server use stdio or Streamable HTTP?

Use stdio for a first server. It runs as a local process the host starts for you, needs no ports, no certificates and no token exchange, and it is the transport the Model Context Protocol documentation names for local processes. Move to Streamable HTTP only when other machines must reach the server.

How do I stop an AI assistant from taking an action I did not approve?

Remove the one-step path from the server itself. Expose a draft tool that returns a preview and a separate confirm tool that performs the action, and ship no tool that does both. WhatsAppMan has no raw send tool for this reason, and MailMan pairs draft_email with confirm_send so nothing leaves until you approve it.