Plinth / guides / build-mcp-server-python

How to build an MCP server in Python

~6 min read · Python 3.11+ · Model Context Protocol

The Model Context Protocol (MCP) lets an AI client — Claude Desktop, an IDE, your own agent — call tools, read resources and use prompts that you expose from a server. In Python you get a working server in about fifteen lines. Getting one you can actually run in front of users takes more. This guide does both: the fast path first, then the gaps.

1. Set up the project

Use a real environment manager. The official SDK ships as mcp:

python -m venv .venv && source .venv/bin/activate
pip install "mcp[cli]"

If you're on Poetry, poetry add "mcp[cli]" is equivalent. Pin your Python to 3.11+ — the async story is much nicer.

2. Define a server and a tool

The high-level FastMCP API is the quickest way in. A tool is just a typed function with a docstring — the SDK turns the signature into a JSON schema the client sees.

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather")

@mcp.tool()
def get_forecast(city: str, days: int = 1) -> str:
    """Return a short forecast for a city."""
    # call your real API here
    return f"{city}: clear, {days}d ahead"

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

That's a valid MCP server. mcp.run() speaks JSON-RPC over stdio — stdin and stdout — which is how desktop clients launch and talk to local servers.

3. Run it and connect a client

For a fast feedback loop, the SDK ships an inspector:

mcp dev server.py

To use it from a desktop client, register the command (python server.py) in the client's MCP config. The client spawns your process and pipes JSON-RPC through stdio.

The gotcha that breaks everyone Because stdio is the protocol channel, anything you write to stdout — a stray print(), a library banner — gets injected into the JSON-RPC stream and the client disconnects. All logging must go to stderr.

4. Add tools, resources and prompts

Beyond tools, MCP has resources (readable data the model can pull in, like files or rows) and prompts (reusable templated messages). The decorators mirror tools: @mcp.resource("scheme://{id}") and @mcp.prompt(). Keep tool functions small and validate their inputs — the model will send you arguments you didn't expect.

5. Where the quickstart ends and production begins

The code above runs. It is not ready for anyone but you. Before it touches real traffic you need:

None of this is hard individually. Together it's the week of unglamorous work that stands between "it runs on my machine" and "it runs for users." Write a test that drives the server over the real protocol so you know your changes hold.

Or start from a base that already has all of it

The Plinth MCP Server Starter Kit is a typed, tested Python template (plus a matching TypeScript one) with auth gating, rate limiting, backoff, structured stderr logging, graceful shutdown, input validation, health checks and a hardened Dockerfile already wired up — and 23 Python tests driving it over the real MCP protocol.

Get the kit — $39