How to Build an MCP Server
Most MCP tutorials predate the 2026-07-28 revision, which replaced the init handshake with server/discover and deprecated sampling and logging. What to build now, why the tool description is the hard part, and the security you can't skip.
Most MCP tutorials you will find were written against an earlier revision of the protocol. The specification moved on 2026-07-28 in ways that change real code — the initialisation handshake was replaced, sampling and logging were deprecated, and notifications became opt-in subscriptions. If you follow a guide from 2025 you will write something that still works through SDK compatibility shims but is not how the protocol works now.
This is the current shape of it. If you need the conceptual grounding first, what is MCP and why it matters covers why the protocol exists at all.
What you are actually building
MCP is a client-server protocol over JSON-RPC 2.0. Three participants:
- MCP Host — the AI application (Claude Code, Claude Desktop, VS Code) that coordinates one or more clients
- MCP Client — one per server, maintaining a single connection
- MCP Server — your program, which
provides context to MCP clients
A server exposes three primitives:
- Tools — executable functions the model can invoke (file operations, API calls, database queries)
- Resources — data sources that provide context (file contents, records, API responses)
- Prompts — reusable templates that structure an interaction
Most servers only implement tools, and that is a perfectly good place to start. Each primitive has */list for discovery, */get or */read for retrieval, and tools/call for execution.
Pick your transport first
This decision shapes everything else.
stdio runs your server as a local subprocess, communicating over standard input and output. One client, no network, no auth, minimal overhead. This is right for anything touching the local machine — filesystem, git, a local database — and it is the easier path for a first server.
Streamable HTTP uses HTTP POST with optional Server-Sent Events for streaming. It serves many clients, works remotely, and supports standard HTTP authentication — bearer tokens, API keys, custom headers. The spec recommends OAuth for obtaining tokens. Choose this if the server wraps a hosted service or needs to be shared.
What changed in 2026-07-28
Worth knowing before you copy code from anywhere:
server/discover replaced the initialisation handshake. Every server must implement it. It returns supported protocol versions, capabilities, and server identity in one request, and the response is cacheable via ttlMs and cacheScope.
The protocol is now stateless. Every request carries its own _meta with io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities, and normally clientInfo. The server infers nothing from previous requests. Calling server/discover is optional as a result — a client can send any request directly and handle a version error if one comes back.
Sampling and logging are deprecated. Sampling let a server request a completion from the client's model; new implementations should integrate with an LLM provider directly. Logging should now go to stderr on stdio, or OpenTelemetry.
Elicitation is the remaining client primitive — how a server asks the user for more information or confirms an action, via elicitation/create.
Notifications are opt-in. A client opens a long-lived subscriptions/listen stream naming the event types it wants; the server acknowledges with the subset it will honour. Delivery is explicitly best-effort, so clients should still poll for freshness.
There is also a Tasks extension for long-running work: the server returns a durable handle the client can poll, instead of holding a request open.
The build
Use an official SDK — Python, TypeScript, and others are maintained at modelcontextprotocol.io. They handle JSON-RPC framing, the _meta plumbing, and version negotiation, which you should not be writing by hand.
The core of a server is a set of tool definitions. Each one needs a name unique within your namespace, a human-readable title, a description explaining what it does and when to use it, and an inputSchema — JSON Schema defining the parameters.
The official docs make a naming point worth absorbing: prefer calculator_arithmetic over calculate. Tool names collide across servers in a host that has ten of them loaded, and a namespaced name is the only thing preventing ambiguity.
Tool responses return a content array of typed objects, so a single call can return text, images, or embedded resources.
The part that determines whether it actually works
Here is the thing that separates a server the model uses correctly from one it misuses constantly: the tool description is a prompt.
It is not documentation for a human reading your README. It is text injected into the model's context, and the model decides whether and how to call your tool based on nothing else. If two of your tools have descriptions that could plausibly cover the same request, the model will pick wrong, and it will do so consistently.
So write descriptions that say what the tool does, when to use it, and — the part everyone omits — when not to. Keep schemas tight and typed, with enums where the values are fixed and descriptions on individual parameters. Return errors as informative text the model can act on rather than a stack trace, because the model is your error handler.
How to design tools your AI agent can actually use goes deeper on this, and it applies to MCP tools without modification.
Two more principles that matter in practice. Do not expose every backend function — a server with 60 tools consumes an enormous amount of context and makes the model's selection problem harder; expose the handful of workflows people actually need. And design return values for a model, not a UI — a trimmed, labelled subset beats a raw API payload with forty null fields.
Testing it
Use the MCP Inspector — npx @modelcontextprotocol/inspector — which gives you a UI for listing tools, invoking them, and inspecting JSON-RPC traffic without an LLM in the loop. Get the protocol correct there first. Debugging a schema error through a model that is also guessing at your intent is unnecessarily hard.
Then connect it to a real host and watch which tools actually get selected. The gap between my tool works when called
and the model calls my tool at the right time
is where most of the work is, and it is a prompt-engineering problem rather than a coding one.
Security, which is not optional here
An MCP server is a privilege boundary, and the threat model is not obvious.
Tool descriptions are read by the model before any tool is called. A malicious or compromised server can therefore influence the host's behaviour without ever being invoked. Tool results are equally untrusted — whatever your server returns goes straight into the model's context, and if it fetched that content from the web, an attacker wrote part of your prompt.
This is the prompt injection problem, and MCP gives it a convenient delivery mechanism. Concretely, when building a server: scope credentials to the minimum the tools need, prefer read-only operations, validate and constrain inputs at the tool boundary rather than trusting the schema alone, and use elicitation/create to get explicit user confirmation before anything destructive or irreversible. If you serve over HTTP, authenticate properly — an unauthenticated remote MCP server is an open API with a natural-language front door.
Then list it
Once it works, it needs to be findable. Ours is at the MCP server directory, alongside several hundred others.
For the wider picture of where a server fits — how tools compare with retrieval, how the loop calls them — see rag vs tools vs long context, the agentic loop explained, and the AI development pack.
Sources
- Model Context Protocol, Architecture overview — hosts, clients and servers; the data and transport layers; the three server primitives; statelessness,
server/discover, opt-in subscriptions, and the deprecation of sampling and logging as of protocol revision2026-07-28 - Model Context Protocol, Build an MCP server — the official quickstart, tool definition shape, and SDK guidance
- Model Context Protocol, MCP Inspector — testing a server without an LLM in the loop