A working MCP server is about twenty lines of Python now. You import one class, decorate a function, and run it. That is the entire on-ramp.
Which is the problem.
The Model Context Protocol made it so easy to expose a tool to an agent that nobody stops to ask whether the tool should be a tool. The result is an ecosystem closing in on twenty thousand servers, most of which wrap something the agent already knew how to do. So let's build one properly, and then run the test almost nobody runs: does this thing earn a place in the context window at all?
The whole build, in one file
Here is the official quickstart, stripped to the bone. FastMCP ships in the Python SDK and does the boring work for you.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
"""Get the weather forecast for a location."""
# call your weather API here
return f"Forecast for {latitude},{longitude}: clear, 18C"
if __name__ == "__main__":
mcp.run(transport="stdio")That is a complete, working server. The @mcp.tool() decorator is doing the part that used to be tedious: FastMCP reads your type hints and your docstring and generates the tool schema from them. The function signature is the contract. latitude: float becomes a typed parameter the model has to fill correctly, and the docstring becomes the description the model reads to decide when to call it.
MCP gives you three primitives, not one. Tools are the famous one, but the other two matter:
- Tools are functions the model calls, with user approval. Actions with side effects.
- Resources are file-like data the client can read: an API response, a config file, the contents of a table. Decorate with
@mcp.resource("scheme://path"). - Prompts are reusable templates a user can invoke deliberately. Decorate with
@mcp.prompt().
The split is the whole design. A resource is something the agent reads. A tool is something the agent does. If you are exposing read-only data and you reach for @mcp.tool(), you have already modelled it wrong.
To run it, you point a client at the command. For Claude Desktop that is a few lines of JSON naming the executable and its args. The transport="stdio" line is the one that matters: the client launches your script and talks to it over standard in and out. That detail looks innocent. It is also, as we will get to, where a lot of the danger lives.
That is the tutorial. You can have a custom server talking to an agent in the time it takes to read this far.
It works. That is the problem
Twenty lines to a working server sounds like a win, and for the right tool it is. But the ease is also why the registry is drowning. When the cost of shipping a server rounds to zero, people ship a server for everything, including the long list of things that were already solved.
The agent in front of you can already run shell commands. It can already read files. It can already call curl, jq, git, gh, psql, and every other CLI on the box. A huge share of the MCP servers in the wild are a thin async wrapper around a command-line tool the model already knows how to drive, now reincarnated as a dependency you install, run as a process, and trust.
So before you write the decorator, run the test.
The test: should this be a tool at all?
Ask one question. Can the agent already do this with something it has?
If the answer is "yes, it could just run the CLI", you do not need a server. You need to let the agent run the CLI. Wrapping git log in an MCP tool does not make the agent better at git. It adds a process, a dependency, and a permanent line item in the tool list, in exchange for nothing the Bash tool wasn't already giving you.
An MCP server earns its place when it provides something the agent genuinely cannot get on its own:
- A credential boundary. The server holds the API key and exposes three safe operations, so the agent never sees the secret. That is real value a shell command can't replicate.
- A custom protocol or internal system. Something with no CLI, no public docs, no way for the model to know the shape of it. Your proprietary system. This is the case the protocol was built for.
- State the agent can't hold. A connection pool, a long-lived session, a cache that has to survive across calls.
- A genuinely awkward surface made safe and small. Not "I wrapped curl", but "I turned a forty-parameter API into the three operations anyone actually needs".
If your server isn't doing one of those, it probably shouldn't exist. It should be a script the agent runs, or a line in a CLAUDE.md, or nothing at all.
Every server you add has a bill
Two of them, actually.
The first is your context window. Every tool descriptor, every name, parameter, and description, gets folded into the model's prompt before it does anything. Ten servers with five tools each is fifty descriptions the model reads on every single turn. That is the same flooding problem I went after in context discipline: the more you bolt on, the less room is left for the actual work, and the worse the model gets at choosing between the noise. A tool the agent rarely needs is not free. It is rent, paid every turn.
The second bill is trust, and it is the one people skip. A tool descriptor is natural language the model reads and acts on. The description field is an execution path. I spent a whole post two days ago on why the MCP supply chain is the new npm: configuration-to-command execution baked into every official SDK, that innocent stdio line handing a stranger's package a shell on your box, 67% of scanned servers carrying code-injection risk. Every server you add to your tool list is a process you are trusting with the most credentialed seat on your machine.
So the question "should this server exist" isn't pedantry. Each one you wave through costs you context on the way in and trust on the way down.
Before you build, look at what is already there
Here is the step the twenty-line tutorial skips. Before you write a server, check whether the thing already exists and whether it is worth trusting. The ecosystem is too big and moves too fast to eyeball.
That is the gap I built MCP Observatory to close. It tracks the whole MCP landscape, getting on for twenty thousand servers across npm, PyPI, GitHub and the official registries, so you can answer two questions before you commit. Does this already exist? Search it and find out before you reinvent a wrapper someone already maintains. Is it safe to wire in? It scores servers on abandonment, licensing gaps, dependency vulnerabilities and CVE feeds, and runs static analysis that flags the exact command-injection and SSRF patterns that turned out to be everywhere, plus the naming games that make typosquatting work.
Use it both ways. As a discovery tool, it stops you building the thing that already exists. As an audit tool, it stops you trusting the thing you shouldn't.
The ease cuts both ways
The same twenty lines that let you stand up a useful, credential-guarding, protocol-bridging server also let anyone stand up a redundant one, or a malicious one, and ship it to a registry with a badge on it. The protocol made building trivial. It did nothing to make deciding trivial, and deciding is the part that was always hard.
So build the server. The tutorial really is that short. Just run the test first, and the test is not "can I build this". You can. It is "should this be a tool, or am I about to make the agent install a process to do something it could already do".
Most of the time, the honest answer is the second one.