How to Build an MCP Server in Python

The Model Context Protocol (MCP) is an open standard that lets AI apps like Claude Desktop, Claude Code and Cursor use your tools and data. Write one MCP server, and every app that speaks MCP can use it.

This guide builds a working server with the official Python SDK v2 (stable since July 2026), tests it, and connects it to real apps. If you've seen older tutorials using FastMCP, read the migration note below: the class has been renamed.


馃幆 Quick Answer (30-Second Read)

  • Install: pip install "mcp[cli]" or uv add "mcp[cli]" (Python 3.11+).
  • Create: mcp = MCPServer("Name"), then decorate functions with @mcp.tool(), @mcp.resource() or @mcp.prompt().
  • Test: uv run mcp dev server.py opens the MCP Inspector.
  • Connect: uv run mcp install server.py for Claude Desktop, or claude mcp add for Claude Code.
  • v2 change: from mcp.server.fastmcp import FastMCP is now from mcp.server import MCPServer.

How MCP Fits Together

flowchart LR U["馃 You"] --> H subgraph H["Host app (Claude Desktop, Claude Code, Cursor)"] LLM["Claude"] <--> C["MCP client"] end C <-->|"stdin / stdout (stdio)"| S["Your MCP server
server.py"] S --> T["Tools
actions Claude can call"] S --> RS["Resources
data Claude can read"] S --> P["Prompts
reusable templates"] T --> API[("Your APIs, DB, files")] RS --> API

The host launches your server as a child process and talks to it over stdin/stdout. You don't open a port or run a web server for local use. The host just needs the command that starts your server.

Building block What it is Who uses it
Tool A function Claude can call (search, create, send) The model decides when
Resource Read-only data addressed by a URI The app or user attaches it
Prompt A reusable prompt template The user picks it

Step 1: Install the SDK

# with uv (recommended)
uv init mcp-bookshop && cd mcp-bookshop
uv add "mcp[cli]"

# or with pip
pip install "mcp[cli]"

The [cli] extra adds the mcp command used to test and install servers.


Step 2: Write the Server

Create server.py:

from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

mcp = MCPServer("Bookshop")

CATALOG = {
    "Dune": "Frank Herbert",
    "Neuromancer": "William Gibson",
    "The Left Hand of Darkness": "Ursula K. Le Guin",
}


@mcp.tool()
def search_books(query: str) -> list[str]:
    """Search the catalog by title or author."""
    needle = query.lower()
    return [t for t, a in CATALOG.items() if needle in t.lower() or needle in a.lower()]


@mcp.tool()
def get_author(title: str) -> str:
    """Look up the author of a book in the catalog."""
    if title not in CATALOG:
        raise ToolError(f"No book titled {title!r} in the catalog.")
    return CATALOG[title]


@mcp.resource("catalog://titles")
def titles() -> str:
    """Every title in the catalog, one per line."""
    return "\n".join(sorted(CATALOG))


@mcp.prompt()
def recommend(genre: str) -> str:
    """Ask for a book recommendation in a genre."""
    return f"Using the bookshop tools, recommend one {genre} book and explain why."


if __name__ == "__main__":
    mcp.run()

What the SDK does for you:

  • The function name becomes the tool name.
  • The docstring becomes the description Claude reads to decide when to use it. Write it clearly.
  • Type hints become the input schema, so Claude knows query must be a string.
  • ToolError sends a clean error message back to Claude instead of crashing.
  • mcp.run() with no arguments starts a stdio server, which is what desktop apps expect.
Pro tip: keep mcp.run() under if __name__ == "__main__":. The mcp CLI imports your file, and an unguarded run() would start a server the moment it's imported.

Step 3: Test It in the MCP Inspector

uv run mcp dev server.py

This opens the MCP Inspector in your browser. You can list your tools, call search_books with test input, read the catalog://titles resource and preview the prompt, all before involving an AI app.


Step 4: Connect It to an App

Every app needs the same launch command:

uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py

Claude Desktop (the SDK writes the config for you):

uv run mcp install server.py

Then fully quit Claude Desktop and reopen it. The entry is saved in claude_desktop_config.json (on macOS: ~/Library/Application Support/Claude/). Need an API key? Add -v API_KEY=abc123 or -f .env, because Claude Desktop doesn't see your shell's environment variables.

Claude Code:

claude mcp add bookshop -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py

Run /mcp inside Claude Code to confirm it's connected.

Cursor: create .cursor/mcp.json in your project:

{
  "mcpServers": {
    "bookshop": {
      "command": "uv",
      "args": ["run", "--with", "mcp[cli]", "mcp", "run", "/absolute/path/to/server.py"]
    }
  }
}

Now ask Claude: "Who wrote Neuromancer?" It will call your get_author tool.


Migrating From v1 (FastMCP)

v1 v2
from mcp.server.fastmcp import FastMCP from mcp.server import MCPServer
mcp.server.fastmcp.* mcp.server.mcpserver.*
ctx.fastmcp ctx.mcp_server
get_context() Declare a ctx: Context parameter
FastMCPError MCPServerError

The @mcp.tool(), @mcp.resource() and @mcp.prompt() decorators work as before. Not ready to migrate? Pin mcp>=1.28,<2.


Common Problems

  • Server doesn't appear: fully quit and reopen the app. Hosts read config only at launch.
  • "uv not found": apps start servers with a minimal PATH. Use the absolute path from which uv.
  • Relative path used: always give the absolute path to server.py.
  • Missing API keys: pass them with -v / -f when installing, or in the config's env field.
  • Printing to stdout: in stdio mode, stdout carries protocol messages. Log to stderr instead.

Going Remote

Local stdio servers are perfect for personal tools. To share a server with people who don't have your file, serve the same mcp object over Streamable HTTP:

mcp.run(transport="streamable-http")

Users then connect with a URL instead of a command. Add authentication before exposing it on the internet.


Frequently Asked Questions

What is the difference between an MCP tool and a resource?

A tool is a function the model can decide to call, such as searching or creating something. A resource is read-only data identified by a URI that the app or user can attach as context. Use tools for actions and lookups, and resources for documents or data to read.

Is FastMCP still available?

In the official MCP Python SDK v2, the FastMCP class was renamed to MCPServer and the old import path was removed. Older tutorials using FastMCP need the new import. You can stay on v1 by pinning the package below version 2.

Does an MCP server work with apps other than Claude?

Yes. MCP is an open protocol. The same server works in any MCP-compatible host, including Claude Desktop, Claude Code, Cursor and VS Code. Each host just needs the command that starts your server, or a URL for remote servers.


Conclusion

Building an MCP server in Python takes one file: create an MCPServer, decorate functions as tools, resources and prompts, test in the Inspector, and register the launch command with your app. Clear docstrings and type hints do most of the work of teaching Claude how to use your tools.


Sources

Last verified: 14 October 2026.


Related reads: Claude Code CLI Tutorial 路 How AI Agents Work 路 How to Create a CLAUDE.md File 路 Claude Models Explained

I write about building with AI and how real products work. Follow on LinkedIn or X, or invite me to speak.