← Back to Blog

Build Your First MCP Server in 10 Minutes with the Python SDK

Building an LLM app, you hit the same wall every time. You hardcode one function call into the prompt, then another, and pretty soon you have built an undocumented integration layer that only your prompt can talk to. Swap models and the wiring breaks. The Model Context Protocol (MCP) exists to kill that sprawl: one standard way for a host app to hand an LLM tools, resources, and reusable prompts.

MCP started as an open spec from Anthropic in late 2024 and settled into a stable protocol revision by mid-2025. Think of an MCP server like a web API, but shaped for LLMs. Resources carry data the model can load, roughly GET endpoints. Tools perform actions, roughly POST. Prompts are reusable templates. Once you expose those through MCP, any compatible host (Claude Desktop, Claude Code, Cursor, or an agent runtime like Hermes) can talk to your server without custom glue.

This tutorial walks through a real server with the official Python SDK, tests it in the MCP Inspector, and connects it to a host. Everything is copy-paste ready, no filler.

Prerequisites

  • Python 3.10 or newer
  • uv or pip for installing the SDK
  • Node.js on your PATH (the Inspector runs on npx, so it is required for the interactive test)

Step 1: Install the SDK

The current stable release is the v2 line of the SDK. Install it with either:

pip install "mcp[cli]"

or, if you prefer uv:

uv add "mcp[cli]"

The [cli] extra adds the mcp command you will use during development.

Step 2: Write the server

Create the file server.py. This is a complete server, not a stub:

from mcp.server import MCPServer

mcp = MCPServer("Demo")

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

@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"

That is it. Two typed functions and docstrings. The SDK turns @mcp.tool() into a JSON Schema-driven tool the host can call, and @mcp.resource() into a data endpoint. You never write schema, request parsing, or protocol handling yourself.

Two details worth internalizing:

  • The type hints are the contract. The signature a: int, b: int -> int tells the host the tool takes two required integers and returns one. The Inspector and every host build their call form from these hints.
  • The docstring is the model's instruction manual. It becomes the tool description the LLM reads to decide when to call it. Write it in terms of intent, not implementation.

Step 3: Test it live

Run the dev loop to open the Inspector:

uv run mcp dev server.py

The Inspector is a browser UI for poking at your server. It needs npx on your PATH, so check that before you run. Open the URL it prints and call add with a=1, b=2. You get 3 back. Read the resource greeting://World and you get "Hello, World!".

You did not write how that happens. The SDK handled JSON-RPC framing, validation, and serialization for you.

Step 4: Point a host at it

Install the server into a host. With the CLI extra you can run mcp install server.py to register it with your default client config, or add it manually through your host's mcpServers config:

{
  "mcpServers": {
    "demo": {
      "command": "/path/to/.venv/bin/python",
      "args": ["-m", "mcp.server", "/path/to/server.py"]
    }
  }
}

The exact config file name and location depend on the host, so check its docs. From here the LLM can add numbers and read your resources the same way it reads its own context.

How the server talks to the client

MCP servers speak two standard transports: stdio and Streamable HTTP (per the 2025-06-18 protocol revision). stdio is the common default: the host launches your server as a subprocess, writes JSON-RPC to its stdin, and reads responses from stdout. One rule to internalize up front: your server must never write anything except MCP messages to stdout. Use stderr for logging. If you break that, you silently corrupt the protocol stream.

When to reach for MCP

MCP shines when you want one backend of tools to serve many different LLM apps without rewriting glue each time, or when you are building a plugin-style extension for a host like Claude Desktop.

It is overkill the day you have exactly one app, one model, and a couple of function calls. A plain function-calling loop in your own code is simpler. Skip the protocol until the second consumer shows up.

Common follow-ups

  • Serve it over HTTP by mounting the server to an existing FastAPI or Starlette app, which the SDK supports for remote access.
  • Use the Context object inside tools to report progress and read resources, handy for long-running operations.
  • Add Prompts (reusable templates) when hosts should surface curated workflows, not just tools.

Next steps

Pick one real capability you keep glue-coding into prompts, expose it as a tool, and wire it to the client you actually use. That first successful host call is the moment it clicks: once one tool flies end to end, the rest of your backlog stops looking like a pile of bespoke wrappers.

Referensi

Need Help Implementing This?

I help teams design and build scalable cloud infrastructure, DevOps pipelines, and production-grade systems.

Book a Free Consultation