Build an MCP Server with Python FastMCP: Step-by-Step

By Yanni Papoutsis | 5 min read | 2026-06-26

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:

  1. Tools: Functions Claude can call (e.g., search_database, send_email, create_file)
  2. Resources: Data sources Claude can read from (e.g., a database table, a file)
  3. 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

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.