The Model Context Protocol went from an Anthropic announcement in November 2024 to a de facto industry standard in about six months. OpenAI committed to it in March 2025; Microsoft and GitHub joined the steering committee at Build in May. That kind of adoption curve usually means the thing is either very hard or very simple. MCP is the second case. If you can write a Python function, you can build an MCP server this afternoon. The hard part is somewhere else, and almost nobody warns you about it.

What you're actually building

MCP is an open standard, built on JSON-RPC, that gives any LLM application a uniform way to connect to external systems. Instead of writing a bespoke integration for every model and every client, you write one server, and any MCP-compatible host — Claude Desktop, an IDE, your own agent — can use it.

A server can expose three kinds of capability, and the spec organizes them by a question most tutorials skip: who decides when this gets used?

Hold onto that distinction. It's the whole game, and we'll come back to why.

The minimal server

The official Python SDK bundles FastMCP, a high-level interface that turns a function into a tool with one decorator. Here is a complete, working server:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Demo")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

if __name__ == "__main__":
    mcp.run()  # stdio transport by default

That's it. FastMCP reads your type hints to generate the tool's input schema and lifts the docstring to use as the tool's description. The decorator needs the parentheses — @mcp.tool(), not @mcp.tool. There's an equally terse TypeScript SDK if Node is your world.

Official Python SDK; bundles FastMCP, the decorator-based high-level server interface
Official TypeScript SDK for MCP servers and clients; runs on Node, Bun, Deno
FastMCP 2.0 — the standalone, fuller-featured successor to the version folded into the Python SDK
★ 26kPythonjlowin/fastmcp

One note on that last repo, because it confuses people: FastMCP 1.0 was merged into the official Python SDK. The standalone jlowin/fastmcp project continued as FastMCP 2.0 with additional features. For a first server, the bundled version is fine; reach for the standalone when you outgrow it.

Pick the right transport

The spec defines two transports, and the choice is about where your server runs:

If you read older guides referencing an "HTTP+SSE" transport with two endpoints, that design was deprecated in March 2025. It was hostile to load balancers, serverless platforms, and firewalls; Streamable HTTP's single-endpoint shape fixes that. Build against Streamable HTTP for anything remote and don't look back. To run the example above over HTTP, that's mcp.run(transport="streamable-http").

Test it before you wire it in

Don't debug your server through a chat client. Use the MCP Inspector, the official visual testing tool — npx @modelcontextprotocol/inspector, then open the local UI. It connects to either transport and lets you list and invoke your tools, resources, and prompts by hand, so you can confirm the server works before any model is involved.

Official visual debugger for MCP servers; invoke tools/resources/prompts interactively

When it's ready for Claude Desktop, you register it in claude_desktop_config.json under an mcpServers key, with a command, its args, and any env. Restart the client and your tools appear.

The part nobody warns you about

Here's what the quickstart won't tell you: the quality of your tool's name and description matters more than the code inside it.

The description is the only documentation the model ever sees. It's how the LLM decides whether to call your tool, and with what arguments. A flawless function behind a vague docstring — "does stuff with orders" — will be called at the wrong times, with the wrong inputs, or ignored entirely. A mediocre function with a precise description and well-typed parameters will be used correctly. You are not writing for a compiler. You are writing for a reader who has only the label and must decide, in one shot, whether to pull the lever.

The model never reads your implementation. It reads your description and gambles. Write the description like it's the only thing that's true.

This is also why the Tools-versus-Resources distinction has real consequences. Because only Tools are model-invokable, and because most clients have historically had thin support for Resources and Prompts, builders default to exposing everything as a Tool — including read-only data that is a textbook Resource. It works, but it floods the model's tool list with things it shouldn't have to reason about calling. If a capability is "data the app should load," make it a Resource and let the host decide. Reserve Tools for actions the model should genuinely choose to take.

And if you're building a remote server: it inherits none of the local trust a stdio subprocess gets for free. Authentication is your job now. Don't ship a networked server with the security posture of a script on your laptop.

Build the server in an afternoon. Spend the next afternoon on the descriptions. That ratio is backwards from what it feels like it should be, which is exactly why so many MCP servers technically run and practically don't.