Build an MCP Server with Python FastMCP: Step-by-Step
TL;DR: FastMCP is a Python library that wraps the Model Context Protocol specification into a clean decorator-based API. You define tools as Python functions with @mcp.tool(), run the server, and Claude can call those functions. This is the fastest path to building a custom MCP integration for any Python-compatible API or service. This guide takes you from zero to a working MCP server with multiple tools in under 30 minutes.
What You Are Building
An MCP server is a process that Claude calls via a defined protocol to execute tools. When you build an MCP server, you define:
- Tools: Functions Claude can call (e.g.,
search_database,send_email,create_file) - Resources: Data sources Claude can read from (e.g., a database table, a file)
- Prompts: Pre-written prompts Claude can load on demand
FastMCP handles the protocol layer. You write Python. FastMCP translates it into MCP-compliant tool definitions and handles the stdio/HTTP transport.
Prerequisites
- Python 3.10+
piporuv- Claude Code / Cowork (to test the server)
Step 1: Install FastMCP
pip install fastmcp
Or with uv (recommended):
uv add fastmcp
Verify:
python -c "import fastmcp; print(fastmcp.__version__)"
Step 2: Create Your First MCP Server
Create a file my_server.py:
from fastmcp import FastMCP
# Create the MCP server instance
mcp = FastMCP("My First Server")
@mcp.tool()
def add_numbers(a: int, b: int) -> int:
"""Add two numbers together and return the result."""
return a + b
@mcp.tool()
def greet_user(name: str, formal: bool = False) -> str:
"""
Generate a greeting for a user.
Args:
name: The user's name
formal: If True, use a formal greeting. Default is casual.
"""
if formal:
return f"Good day, {name}. How may I assist you?"
return f"Hey {name}! What's up?"
if __name__ == "__main__":
mcp.run()
Test it locally:
python my_server.py
The server starts and listens on stdio. You will not see output until Claude connects.
Step 3: Connect to Claude
Add your server to ~/.claude/mcp_servers.json:
{
"mcpServers": {
"my-server": {
"command": "python",
"args": ["/absolute/path/to/my_server.py"],
"env": {}
}
}
}
Use absolute paths. Relative paths do not work reliably in MCP config.
Restart Claude Code. In a new session, test:
Use my-server to add 42 and 58.
Claude should call the add_numbers tool and return 100.
Step 4: Add a Real Tool (API Integration)
Here is a more practical example: a tool that queries a hypothetical REST API.
from fastmcp import FastMCP
import httpx
import os
mcp = FastMCP("My API Server")
API_BASE = os.environ.get("MY_API_BASE", "https://api.example.com")
API_KEY = os.environ.get("MY_API_KEY", "")
@mcp.tool()
async def search_products(query: str, max_results: int = 10) -> list[dict]:
"""
Search products in the catalogue.
Args:
query: Search terms
max_results: Maximum number of results to return (default 10, max 50)
Returns:
List of product objects with id, name, price, and description
"""
max_results = min(max_results, 50) # enforce cap
async with httpx.AsyncClient() as client:
response = await client.get(
f"{API_BASE}/products/search",
params={"q": query, "limit": max_results},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10.0
)
response.raise_for_status()
return response.json()["results"]
if __name__ == "__main__":
mcp.run()
Pass environment variables in the MCP config:
{
"mcpServers": {
"my-api-server": {
"command": "python",
"args": ["/path/to/my_server.py"],
"env": {
"MY_API_BASE": "https://api.myservice.com",
"MY_API_KEY": "your-actual-key-here"
}
}
}
}
Security note: Do not commit mcp_servers.json to version control if it contains API keys. Use environment variables or a secrets manager.
Step 5: Add Resources
Resources let Claude read data from your server without calling a tool. Good for reference data, documentation, or structured datasets.
from fastmcp import FastMCP
from fastmcp.resources import FileResource
mcp = FastMCP("My Server")
# Expose a local file as a readable resource
mcp.add_resource(FileResource(
uri="file:///Users/myuser/Desktop/config.json",
name="App Config",
description="Current application configuration"
))
# Or create a dynamic resource
@mcp.resource("data://products/catalogue")
async def get_catalogue() -> str:
"""Return the current product catalogue as JSON."""
# fetch from database or API
return '{"products": [{"id": 1, "name": "Widget"}]}'
Step 6: Error Handling
Your tools should handle errors gracefully. FastMCP catches exceptions and returns them as error responses to Claude. However, descriptive error messages help Claude recover better:
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError
mcp = FastMCP("My Server")
@mcp.tool()
async def get_user(user_id: int) -> dict:
"""Fetch a user by ID."""
if user_id <= 0:
raise ToolError(f"Invalid user_id: {user_id}. Must be a positive integer.")
# ... fetch user from database
user = fetch_from_db(user_id)
if user is None:
raise ToolError(f"No user found with id {user_id}.")
return user
ToolError is returned to Claude as a structured error response. Claude can then decide whether to retry with corrected parameters or inform the user.
Step 7: Type Hints Are Your Documentation
FastMCP uses Python type hints and docstrings to generate the tool schema that Claude receives. Write thorough docstrings:
@mcp.tool()
def calculate_roi(
initial_investment: float,
final_value: float,
time_years: float = 1.0
) -> dict:
"""
Calculate return on investment (ROI) and annualised return.
Args:
initial_investment: The starting investment amount in USD
final_value: The ending value in USD
time_years: Investment duration in years (default 1.0)
Returns:
Dictionary with keys:
- roi_percent: Total ROI as a percentage
- annualised_return: Annualised return rate as a percentage
- profit_loss: Absolute profit or loss in USD
"""
profit_loss = final_value - initial_investment
roi = (profit_loss / initial_investment) * 100
annualised = ((final_value / initial_investment) ** (1 / time_years) - 1) * 100
return {
"roi_percent": round(roi, 2),
"annualised_return": round(annualised, 2),
"profit_loss": round(profit_loss, 2)
}
Claude reads the docstring and type hints to understand what arguments to pass and what output to expect.
Deployment Options
Local (Development)
Run via python my_server.py. Claude connects via stdio. Fast for development.
Persistent Local (Production)
Use launchd (macOS) or systemd (Linux) to keep the server running:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.myname.mcpserver</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/myuser/mcp-servers/my_server.py</string>
</array>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
HTTP Mode (Remote)
FastMCP supports HTTP transport for remote deployment:
if __name__ == "__main__":
mcp.run(transport="http", host="0.0.0.0", port=8080)
Then in MCP config:
{
"mcpServers": {
"remote-server": {
"url": "http://your-server.com:8080/mcp",
"transport": "http"
}
}
}
Troubleshooting
Claude does not see the tools:
- Verify the absolute path in mcp_servers.json
- Run the server manually (python my_server.py) and check for import errors
- Restart Claude Code completely
Tool returns wrong type:
- FastMCP serialises return values to JSON. If you return a complex object, ensure it is JSON-serialisable. Use dict, list, str, int, float, or bool.
Async vs sync:
- FastMCP supports both. Use async def for any tool that makes network calls or does file I/O. Blocking I/O in a sync tool will freeze the server.
Next: Node SDK Version
If you prefer TypeScript, see Build an MCP Server with Node SDK for the JavaScript equivalent.