Guide: Build Your First MCP Server in Python With FastMCP — Step-by-Step (2026)
TL;DR: This guide walks through building a custom Model Context Protocol (MCP) server in Python using FastMCP — from installation to deployment. By the end, you'll have a working MCP server with custom tools, resources, and prompts that any MCP-compatible AI client (Claude Desktop, Cursor, custom agents) can connect to.
The Model Context Protocol has become the standard way for AI agents to interact with external tools and data. While the MCP 101 beginner's guide covers the fundamentals, this guide focuses on the practical side — writing actual Python code, adding real tools, connecting to clients, and deploying.
Prerequisites
- Python 3.11+ installed on your system
- A virtual environment manager (venv, uv, or conda)
- Basic familiarity with Python functions and type hints
- An MCP-compatible client for testing (Claude Desktop, Cursor, or the MCP Inspector)
Step 1: Set Up Your Environment
Create a new project directory and install FastMCP — the leading Python framework for building MCP servers:
class="language-bash">mkdir my-mcp-server
cd my-mcp-server
python -m venv .venv
source .venv/bin/activate
pip install fastmcp
FastMCP handles all the MCP protocol details — message framing, JSON-RPC, schema generation, capability negotiation — so you can focus on writing tools. As of FastMCP 3.0 (January 2026), it supports strict type validation, OpenTelemetry instrumentation, and multiple transport modes out of the box.
Step 2: Create Your First Tool
Create a file called server.py with a minimal MCP server:
class="language-python">from fastmcp import FastMCPmcp = FastMCP(“MyFirstServer”)
@mcp.tool() def add(a: int, b: int) -> int: """Add two integers together. Use for any math calculation.""" return a + b
if name == “main”: mcp.run()
That's it. The @mcp.tool() decorator registers the add function as an MCP tool. FastMCP automatically generates the JSON schema from Python type hints and the docstring becomes the tool description that the AI client reads to decide when to call it.
Run the server to test it:
class="language-bash">python server.py
By default FastMCP uses stdio transport, meaning the server listens on stdin and writes responses to stdout — perfect for local use with desktop AI clients.
Step 3: Add a Resource
Tools let the AI do things. Resources let the AI read things. Add a resource that exposes a configuration file:
class="language-python">import jsonCONFIG = { “app_name”: “MyApp”, “version”: “1.0.0”, “database_url”: “sqlite:///local.db”, “max_retries”: 3 }
@mcp.resource(“config://app”) def get_config() -> str: """Return the application configuration as JSON.""" return json.dumps(CONFIG, indent=2)
The URI scheme (config://app) is arbitrary — you define it. The AI client uses this URI to read data without executing code. Resources are analogous to GET endpoints in REST APIs.
Step 4: Build a Real Tool — SQL Query Executor
Let's build something practical — a read-only SQL query tool:
class="language-python">import sqlite3@mcp.tool() def query_database(sql: str) -> str: """ Run a read-only SQL query against the local database. Only SELECT statements are allowed for safety. """ if not sql.strip().upper().startswith(“SELECT”): return json.dumps({“error”: “Only SELECT queries are allowed”})
conn = sqlite3.connect(“local.db”) cursor = conn.cursor() try: cursor.execute(sql) columns = [desc[0] for desc in cursor.description] rows = cursor.fetchall() return json.dumps({“columns”: columns, “rows”: rows}) except Exception as e: return json.dumps({“error”: str(e)}) finally: conn.close()
This tool lets any MCP-connected AI agent query your local database. The safety check ensures read-only operations, and the structured JSON response makes results easy for the AI to reason about.
Step 5: Connect to an AI Client
To use your server with Claude Desktop, add it to the Claude config file (claude_desktop_config.json):
class="language-json">{
"mcpServers": {
"my-server": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}
For Cursor, configure it in .cursor/mcp.json:
class="language-json">{
"mcpServers": {
"my-server": {
"type": "python",
"command": "uv run /absolute/path/to/server.py"
}
}
}
Restart the client and you should see a tool indicator (a hammer icon 🛠️ in Claude, a tool list in Cursor). Try asking: "What's in my database?" — the AI will discover your tools and use them automatically.
Step 6: Test With MCP Inspector
FastMCP includes a built-in debugger called the MCP Inspector. Launch it alongside your server:
class="language-bash">fastmcp dev server.py
This opens a web interface at http://127.0.0.1:6274 where you can manually invoke tools, browse resources, and test prompts — all without connecting a client.
Pros & Cons of FastMCP
Frequently Asked Questions
Should I run my MCP server locally or remotely?
Local (stdio) is best for personal use — it's fast, secure, and simple. Remote (Streamable HTTP) is needed for shared teams, cloud-hosted agents, or production deployments. Start locally, migrate to remote when you need sharing.
What's the difference between stdio and Streamable HTTP?
Stdio pipes JSON-RPC over stdin/stdout — the client spawns the server as a subprocess. Streamable HTTP exposes the server over a network endpoint. The earlier HTTP+SSE transport was deprecated in the 2025-03-26 spec; new remote servers should use Streamable HTTP exclusively.
Can I use existing Python libraries inside my tools?
Absolutely. That's the whole point. Your tools are regular Python functions — you can import requests, pandas, scikit-learn, or any library. The MCP layer only handles the transport and schema; the logic is just Python.
How should I handle errors in my MCP tools?
Return structured error JSON instead of raising exceptions. The AI client needs to understand why a tool failed so it can retry or adjust. Use {"error": "description", "code": "ERROR_CODE"} as the return type.
How do I deploy my MCP server to production?
Package your server with dependencies in a Docker container, expose it on a port with Streamable HTTP transport, and deploy to any cloud platform (Render, Railway, Fly.io, or your own VPS). Set mcp.run("http://0.0.0.0:8000") for HTTP transport and configure authentication.