- A working server is about thirty lines. The path is uv init, uv add the SDK, decorate two functions as tools, run over stdio, point the host at an absolute path.
- The spec changed materially on 28 July 2026. The release candidate removes the initialize handshake and the Mcp-Session-Id header, making the transport stateless so any instance can answer any request.
- If your server needs state after that change, you carry it yourself in an explicit handle such as a basket id, because the protocol no longer holds a session for you.
- Three primitives exist and almost everyone only needs one. Tools are actions the model can call, resources are readable context addressed by URI, prompts are templates the server offers.
- Printing to stdout is the single most common way to break a stdio server, because stdout is the message channel. Log to stderr and the mystery disconnects stop.
- Debug with the Inspector before you touch a host config. The host adds a restart cycle and hides the error, and the Inspector shows you the actual protocol traffic.
The specification reads cleanly and the concept is simple, and then people spend an afternoon on a first server anyway. The time does not go into the code. It goes into a config file with a relative path in it, a stray print statement, and a host application that reports failure as an empty list of tools.
This is the short path, in the order the errors actually arrive. Commands come from the official quickstart, checked on 4 August 2026, and one important thing changed six days before that date, so start with what changed.
What changed in the July 2026 specification?
The transport went stateless. The 2026-07-28 release candidate removes the initialize and initialized handshake along with the Mcp-Session-Id header, so a server can sit behind an ordinary round robin load balancer with no sticky routing and no shared session store.
What replaces it: client information and capabilities travel in _meta fields on every request, and multi step interactions return an InputRequiredResult with the request state encoded in the payload, so any instance can pick up a continuation. Two new headers, Mcp-Method and Mcp-Name, carry routing information. The error code for a missing resource moved from -32002 to the JSON-RPC standard -32602.
For a first server none of this changes the code you write. It changes what you can assume. If your tool needs to remember something between calls, you now hold that yourself behind an explicit handle, the way a shopping basket id works, rather than expecting the protocol to keep a session alive. Tier 1 SDKs were given a ten week window to support the change, which means tutorials written before May 2026 describe a handshake that no longer exists.
What are the three primitives, and which do you need?
Tools, resources and prompts. The official build guide defines them plainly, and its own tutorial then focuses entirely on tools, which tells you something about the ratio in practice.
| Primitive | What it is for | The mistake people make |
|---|---|---|
| Tools | Functions the model can call, with user approval, to do or fetch something | Writing a vague description, so the model never picks the tool or picks it constantly |
| Resources | Readable context addressed by a URI, like a file or a record | Using a resource where a tool was needed, then wondering why nothing acts on it |
| Prompts | Templates the server offers the client for a repeated task | Building them first, before anything works, and shipping a server nobody invokes |
| Transport | How the messages travel, stdio locally or HTTP over a network | Choosing HTTP for a local tool and inheriting an authentication problem for no gain |
Build tools first. Add resources when you notice the model asking for the same background information every time. Add prompts last, if ever. If you want the conceptual layer underneath this rather than the build steps, our explainer on how MCP connects an assistant to real tools covers the protocol design.
The shortest working path
Five commands and two functions. The Python route is the one with the fewest moving parts.
Set up the project. Install uv, then run uv init weather, cd weather, uv venv, and activate it with source .venv/bin/activate. Add the SDK with uv add followed by mcp in square brackets cli, quoted so your shell does not eat the brackets. Create weather.py. The quickstart notes two hard requirements: Python 3.10 or higher, and Python MCP SDK 2.0.0 or higher.
Create the server object. Import MCPServer from mcp.server and instantiate it with a name. That name is what appears in the host, so make it the name of the thing it does rather than the name of your project.
Write a tool. Decorate an async function with the tool decorator. The SDK reads your type hints and your docstring to build the tool definition it advertises, which is the detail worth pausing on: your docstring is not a comment, it is the description the model reads when deciding whether to call you. Write it for a reader who cannot see your code, and name the arguments in it.
Run it. Under a main guard, call run with the stdio transport, then start it with uv run weather.py. It will sit there producing no output, which is correct. A stdio server that prints anything on start is a stdio server that is already broken.
Connect a host. Add an entry to the host configuration naming your server, with the command set to uv and the arguments carrying a directory flag, then the absolute path to the project folder, then run, then the filename. Restart the host application completely.
Complete working versions in Python, TypeScript, Go, Ruby and Rust live in the quickstart resources repository, which is worth cloning even if you intend to write your own, because diffing your file against a working one finds a typo faster than reading yours again.
Why does the host show no tools at all?
Four causes account for most of it, and they present almost identically, which is what makes this stage expensive.
You printed to stdout. This is the big one, and the documentation is blunt about it: for a stdio server, never write to stdout, because that is where the JSON-RPC messages live and anything else you put there corrupts them. The print function writes to stdout by default, so a single debug line breaks the server in a way that looks like a connection failure. Use the standard logging module, which writes to stderr, with one logger per module.
Your path is relative. The host launches your server from its own working directory, not yours. The config needs the absolute path to the project folder, and on Windows it needs doubled backslashes or forward slashes in the JSON. You may also need the full path to the uv executable itself, which you get from which uv on macOS or Linux and where uv on Windows.
You did not fully restart the host. Closing the window is not the same as quitting the application, and a partially restarted host keeps the old server list.
Your tool description is too vague. The server connects, the tools are listed, and the model never calls one. This is not a bug and no error appears anywhere. A description reading get data will lose to a description reading get active weather alerts for a US state, given a two letter state code, every time.
How do you debug it without the host in the way?
Use the Inspector, which is the piece most first timers discover after they no longer need it. It ships three interfaces from one binary: a web UI, a CLI for automation, and a terminal interface. Running npx followed by the inspector package name starts the web UI, adding a cli flag gives the scriptable version, and a tui flag gives the terminal one.
What it buys you is the protocol traffic. You see whether the server started, what tools it advertised, what arguments a call carried and what came back, without a host application in between deciding how much to tell you. The web UI generates an auth token and prints it to the console; there is an environment variable to disable auth entirely, which is fine on your own machine and should never appear anywhere else.
The working order is: Inspector until the tools list and a call both succeed, then the host config, then real use. Doing it the other way round means every experiment costs a restart.
What does a good tool actually look like?
Narrow, honest about failure, and boring in its return value. Those three properties do more for reliability than any amount of clever prompting on the client side.
Narrow. One tool per question. A single tool taking an action argument that switches between six behaviours reads well in code and badly to a model, which now has to get both the tool choice and the mode right. Six small tools with six clear descriptions outperform one flexible one, and the SDK makes them almost free to write.
Honest about failure. The weather example in the official guide returns a plain sentence when the upstream request fails, rather than raising. That is deliberate. A tool that raises gives the model an error it cannot interpret; a tool that returns unable to fetch alerts for that state gives it something it can relay or work around. Every branch of your function should end in a string a human would understand if they read it out loud.
Boring in its return value. Return the fields, labelled, in a stable order. Resist the urge to format beautifully, because the client will reformat anyway and your formatting only costs tokens. Where a number matters, include its unit in the text rather than assuming it will be inferred.
One more, less obvious. Keep the tool's output small. The result of every call sits in the context window for the rest of the conversation, so a tool that returns two hundred rows has spent the budget for the next ten exchanges. If a query can return a lot, add a limit argument with a sensible default and say so in the description.
Where does the time really go on the first one?
Measured honestly against the steps above, the code is maybe fifteen minutes and everything else is environment.
The install is quick. Writing two tools is quick. What takes the afternoon is the loop between a config file, a host restart and an empty list, because each cycle is slow and none of them tells you anything. That is the whole argument for the Inspector, and it is why this article puts it before the host configuration rather than in a troubleshooting section at the end, where most tutorials leave it.
The second largest cost is version drift in what you are reading. Between the SDK major version requirement, the July specification change and the various tutorials written across a year of a fast moving protocol, a plausible looking guide from six months ago will have you writing a handshake that no longer exists. Check the date on anything you follow, including this, and prefer the official guide when the two disagree.
What about exposing it over a network?
Different problem, and the quickstart says so directly. Its examples are described as intentionally minimal, with a note that exposing a server over HTTP, SSE or WebSocket requires authentication and hardening: CORS allowlists, request size limits, timeouts, rate limits, log redaction.
Worth adding a point the docs do not make. An MCP server is a set of capabilities you are handing to something that decides on its own when to use them, which is the same exposure we wrote about in defending a coding agent against prompt injection. If a tool can write, assume it will eventually be called with arguments that came from text somebody else wrote. Scope the credential the server holds, not the prompt.
If what you actually want is an assistant that can reach your store data rather than a server you maintain, MaShop publishes one. The MaShop MCP page describes what it exposes and how to connect it, which saves building the same thing yourself.
Should you use stdio or HTTP?
Stdio for anything that runs on the same machine as the client, HTTP for anything that does not. That is the whole decision, and picking it wrong is one of the more expensive mistakes on the list because it is invisible at first.
Stdio means the host launches your process and talks to it over standard input and output. There is no port, no certificate, no authentication, and no way for anything else on the network to reach it. The security model is the operating system's process model, which is the strongest one available and the one you get for free.
HTTP means your server is a network service. Now you own the questions the quickstart lists: who is allowed to call it, how large a request may be, how many per minute, how long a call may run, and what appears in the logs. None of that is difficult, and all of it is work you did not have to do a paragraph ago. The stateless change in the July specification makes the HTTP case genuinely nicer to operate, since instances no longer need sticky routing, but nicer to operate is not the same as free.
The pattern that catches people is building a personal tool over HTTP because it feels more real, then leaving it running on a laptop with no auth. If a tool has a credential in it and a port open, it is a service, and it needs treating as one from the first commit rather than after the first surprise.
How do you know it is working properly rather than merely connected?
Three checks, in order, each of which fails differently.
First, does the tool appear. This is a protocol question and the Inspector answers it in one screen. If the list is empty, the server did not start or it broke its own message stream, which sends you back to the stdout rule and the absolute path.
Second, does the model choose it. Ask a question that should obviously trigger the tool and watch whether it does. If the server is connected and the tool is never chosen, the description is the problem, not the code. Rewrite it to name the exact thing it returns and the exact input it needs, then try again. This loop is fast and most people never run it, which is why so many working servers sit unused.
Third, does it behave when the answer is empty or the upstream fails. Call it with an input you know returns nothing. A good tool comes back with a sentence saying so. A bad one raises, and the conversation ends in an error the user cannot act on. Testing the empty case takes thirty seconds and it is the single most skipped step in every tutorial including the official one.
Pass all three and you have something you can rely on tomorrow rather than something that demonstrated well once.
What to build first
Not the weather example, once it runs. The weather server teaches the mechanics and nothing about your work.
Pick the lookup you do most often by hand. The order status query. The stock level for a SKU. The last five support tickets from one customer. One tool, one clear description, read only. It will take an hour and it will be immediately useful, which matters, because the servers people abandon are the ones built to demonstrate the protocol rather than to answer a question they actually had.
Then resist adding a second tool for a week. Watching which arguments the model passes to the first one teaches you more about tool design than any amount of planning, and it usually reveals that your description was worse than you thought. Our practical setup guide covers the configuration side in more depth once you are past the first success.
The last piece of advice is about the calendar rather than the code. The specification is moving quickly, this July brought a change that invalidated the session model, and anything you build against a version number will need a look every few months. Keep your server small enough that a look is cheap. That is the real reason to resist the second tool.