Build an MCP server, then ask whether it should exist
7m read time

Build an MCP server, then ask whether it should exist

A working MCP server is twenty lines of FastMCP. That is exactly the problem. A build tutorial, and the test for whether your server earns a place in the tool list at all.

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.

python
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. For the client side of that story, see logging in to MCP servers from your shell, and for locking down the server side, MCP server authentication.
  • 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 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.

The counter-argument has arrived since: clients now defer tool definitions, so a server you rarely touch barely shows up in your prompt. What a tool surface costs is a separate question from whether the tool earns its place, and a server that got cheap to keep is still a process, a dependency and a schema you own.

(10 of 36)
01My Claude Code setup: status line, plugins and terminal02Superpowers: teaching Claude Code to think before it types03Claude Code hooks: deterministic control over AI workflows04The CLAUDE.md file: give your AI permanent memory05Stop asking your agent nicely06What's new in Claude Code: notes from the London talk07The best number in Opus 4.8 isn't a benchmark08Stale memory is worse than no memory09The agent is just a loop10Build an MCP server, then ask whether it should exist11Skill, subagent, hook, or slash command? Pick the right one12Log in to MCP servers from your shell13How to give Claude safe access to your SQL database14The day 'default' became 'Manual'15Claude Code skills: how to write one that works16How to write a proper Claude Code subagent17Claude Code permissions: the guide I wish the docs were18Sandboxing Claude Code: put your agent in a box that holds19Prompt injection defense for developers who ship agents20Best Claude model for coding: which one for which task21Refactoring legacy code with a coding agent: start with characterization tests22MCP server authentication: OAuth, scopes and rate limits23Opus 5 is here and your effort settings just expired24AI agent incident response: what to do when your coding agent goes wrong25Claude Code /doctor: what it checks and what changed26Claude Code context management: when to /clear and when to /compact27Git worktrees for parallel coding agents: what they isolate and what they share28Claude Code plan mode: decide before the agent writes29Debugging with a coding agent: give it the search, keep the hypothesis30Claude Code checkpoints and /rewind: how to undo an agent's changes31Claude Code cross-session messaging: how to make your sessions talk to each other32Audit logging for AI agents: what Claude Code records and what deserves a human33Sharing Claude Code config across a team: what a repo can and cannot enforce34Your Claude Code session has no clock35Claude Code in a large codebase: scoping an agent to the part that matters36Recovering a Claude Code session your picker will not show you