Building MCP Servers
Pillar Guide

How to Build Your First MCP Server in Python (Step-by-Step Tutorial)

A complete, beginner-friendly tutorial for building an MCP server in Python using the official SDK. Includes code, testing with Inspector, and Claude Desktop setup.

25 min read
Updated February 25, 2026
By MCP Server Spot

Building an MCP (Model Context Protocol) server in Python is the fastest way to give AI assistants like Claude the ability to interact with your data, APIs, and tools. In under 30 minutes, you can go from zero to a fully working MCP server that Claude Desktop can use to perform real actions on your behalf.

This tutorial walks you through every step: from installing prerequisites to testing your server with the MCP Inspector and connecting it to Claude Desktop. By the end, you will have a working weather-lookup MCP server and the knowledge to build any custom server you need.

If you are new to MCP, start with our What Is an MCP Server? guide to understand the foundational concepts before diving into code.

Prerequisites and Environment Setup

Before writing any code, you need two things installed on your system: Python 3.10+ and the uv package manager.

Installing Python 3.10+

MCP's Python SDK requires Python 3.10 or higher. Check your installed version:

python3 --version

If you need to install or upgrade Python:

  • macOS: brew install python@3.12
  • Ubuntu/Debian: sudo apt install python3.12
  • Windows: Download from python.org

Installing uv

The official MCP documentation recommends uv, a blazing-fast Python package manager built by Astral. Install it with a single command:

# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

Verify the installation:

uv --version

Why uv instead of pip? The uv tool automatically manages virtual environments, resolves dependencies faster, and provides the uvx command you will use to run MCP Inspector. It is the recommended approach throughout the MCP ecosystem.

Step 1: Create Your Project

Initialize a new project directory with uv:

# Create and enter the project directory
uv init mcp-weather-server
cd mcp-weather-server

This creates a standard Python project structure with a pyproject.toml file. Next, add the MCP Python SDK as a dependency:

uv add "mcp[cli]" httpx

The mcp[cli] package includes:

  • The core MCP SDK with the FastMCP class
  • CLI utilities for running and testing servers
  • The MCP Inspector dev tool

We also added httpx for making async HTTP requests to weather APIs.

Your project structure should now look like this:

mcp-weather-server/
  pyproject.toml
  .python-version
  hello.py        # default file from uv init (you can delete this)

Step 2: Create the Server File

Create a new file called server.py in your project root. This will contain your entire MCP server:

# server.py
from mcp.server.fastmcp import FastMCP
import httpx

# Create the MCP server instance
mcp = FastMCP(
    "Weather Server",
    dependencies=["httpx"],
)

# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "mcp-weather-server/1.0"


async def make_nws_request(url: str) -> dict | None:
    """Make a request to the National Weather Service API."""
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/geo+json",
    }
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except (httpx.HTTPError, ValueError):
            return None


def format_alerts(alerts: list[dict]) -> str:
    """Format weather alerts into a readable string."""
    if not alerts:
        return "No active weather alerts for this area."

    formatted = []
    for alert in alerts:
        props = alert.get("properties", {})
        formatted.append(
            f"Event: {props.get('event', 'Unknown')}\n"
            f"Area: {props.get('areaDesc', 'Unknown')}\n"
            f"Severity: {props.get('severity', 'Unknown')}\n"
            f"Description: {props.get('description', 'No description')}\n"
            f"Instructions: {props.get('instruction', 'No specific instructions')}\n"
        )
    return "\n---\n".join(formatted)


@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get active weather alerts for a US state.

    Args:
        state: Two-letter US state code (e.g., CA, NY, TX)
    """
    url = f"{NWS_API_BASE}/alerts/active?area={state.upper()}"
    data = await make_nws_request(url)

    if not data or "features" not in data:
        return "Unable to fetch alerts. Please check the state code and try again."

    return format_alerts(data["features"])


@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get the weather forecast for a specific location.

    Args:
        latitude: Latitude of the location (-90 to 90)
        longitude: Longitude of the location (-180 to 180)
    """
    # First, get the grid point for the coordinates
    point_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    point_data = await make_nws_request(point_url)

    if not point_data or "properties" not in point_data:
        return (
            f"Unable to fetch forecast data for coordinates "
            f"({latitude}, {longitude}). Ensure these are valid US coordinates."
        )

    # Get the forecast URL from the point data
    forecast_url = point_data["properties"].get("forecast")
    if not forecast_url:
        return "Unable to determine forecast URL for this location."

    # Fetch the actual forecast
    forecast_data = await make_nws_request(forecast_url)
    if not forecast_data or "properties" not in forecast_data:
        return "Unable to fetch the forecast. Please try again later."

    # Format the periods
    periods = forecast_data["properties"].get("periods", [])
    if not periods:
        return "No forecast periods available."

    formatted = []
    for period in periods[:5]:  # Show next 5 periods
        formatted.append(
            f"{period['name']}:\n"
            f"  Temperature: {period['temperature']}°{period['temperatureUnit']}\n"
            f"  Wind: {period['windSpeed']} {period['windDirection']}\n"
            f"  Forecast: {period['detailedForecast']}\n"
        )

    return "\n".join(formatted)


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

Let us break down the key parts of this code.

The FastMCP Instance

mcp = FastMCP("Weather Server", dependencies=["httpx"])

FastMCP is the high-level entry point for building MCP servers. The first argument is your server's name, which appears in client UIs. The dependencies parameter tells MCP Inspector which packages to install when running your server.

Defining Tools with @mcp.tool()

@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get active weather alerts for a US state.

    Args:
        state: Two-letter US state code (e.g., CA, NY, TX)
    """

The @mcp.tool() decorator registers a Python function as an MCP tool. The SDK automatically:

  1. Extracts the tool name from the function name (get_alerts)
  2. Generates a JSON Schema from the type hints (state: str)
  3. Uses the docstring as the tool description for the AI model
  4. Parses Args sections in the docstring for parameter descriptions

This is the key design insight of FastMCP: your existing Python knowledge transfers directly. Type hints become schemas, docstrings become descriptions, and return values become tool responses.

Async Support

All tool functions use async def because the MCP protocol is inherently asynchronous. This allows your server to handle multiple requests concurrently, which is essential for tools that make network calls or perform I/O operations.

Step 3: Add a Resource

Resources let you expose data that AI applications can read. Add this to your server.py, above the if __name__ block:

@mcp.resource("weather://info")
def get_weather_info() -> str:
    """Information about this weather server and its capabilities."""
    return """
    Weather Server v1.0
    ===================
    This server provides access to US weather data via the
    National Weather Service API.

    Available tools:
    - get_alerts: Get active weather alerts by US state code
    - get_forecast: Get weather forecast by latitude/longitude

    Data source: National Weather Service (weather.gov)
    Coverage: United States only
    Rate limits: Be respectful of NWS API usage guidelines
    """

Resources use URIs (like weather://info) as identifiers. Unlike tools, resources are application-controlled -- the host application decides when to read them, rather than the AI model deciding when to call them.

Step 4: Add a Prompt Template

Prompts provide reusable templates that help users interact with your server effectively. Add this before the if __name__ block:

from mcp.server.fastmcp.prompts import base


@mcp.prompt()
def severe_weather_check(state: str) -> list[base.Message]:
    """Check for severe weather in a US state and provide safety recommendations."""
    return [
        base.UserMessage(
            content=f"Check for any active weather alerts in {state.upper()} "
            f"and provide a summary. If there are severe weather alerts, "
            f"include safety recommendations."
        )
    ]

Prompt templates are user-controlled -- they appear in the client UI and users can select them to start a conversation with a pre-built context.

Step 5: Test with MCP Inspector

The MCP Inspector is a web-based debugging tool that lets you test your server interactively without connecting to any AI client. Launch it with:

mcp dev server.py

This starts two processes:

  1. Your MCP server (connected via stdio transport)
  2. A web UI at http://localhost:5173

Open the Inspector in your browser. You will see three tabs:

TabPurpose
ToolsList and call your registered tools
ResourcesBrowse and read your exposed resources
PromptsView and test your prompt templates

Testing a Tool

  1. Click the Tools tab
  2. Select get_alerts from the list
  3. Enter a state code (e.g., CA) in the state field
  4. Click Run
  5. View the formatted response showing active weather alerts

Testing a Resource

  1. Click the Resources tab
  2. You should see weather://info listed
  3. Click it to read the resource content

If everything works in the Inspector, your server is ready for Claude Desktop.

For more advanced debugging techniques, see our Testing & Debugging MCP Servers guide.

Step 6: Connect to Claude Desktop

To use your MCP server with Claude, you need to add it to the Claude Desktop configuration file.

Locate the Configuration File

The claude_desktop_config.json file is located at:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json

If the file does not exist, create it.

Add Your Server Configuration

Open the config file and add your server under the mcpServers key:

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

Important: Replace /absolute/path/to/mcp-weather-server with the actual absolute path to your project directory. Relative paths will not work.

Restart Claude Desktop

After saving the configuration file, completely restart Claude Desktop (not just close the window -- quit the application and reopen it). Claude needs to restart to detect new server configurations.

Verify the Connection

When Claude Desktop restarts, look for the hammer icon in the input area. Click it to see the list of available tools. You should see get_alerts and get_forecast listed under your "Weather Server."

Try asking Claude:

"What are the current weather alerts in California?"

Claude will recognize it needs to use the get_alerts tool, call it with state: "CA", and present the results in a natural language response.

Step 7: Understanding the Full Server Code

Here is the complete, final version of server.py with all components:

# server.py — Complete MCP Weather Server
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base
import httpx

# Initialize the server
mcp = FastMCP(
    "Weather Server",
    dependencies=["httpx"],
)

# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "mcp-weather-server/1.0"


async def make_nws_request(url: str) -> dict | None:
    """Make a request to the National Weather Service API."""
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/geo+json",
    }
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except (httpx.HTTPError, ValueError):
            return None


def format_alerts(alerts: list[dict]) -> str:
    """Format weather alerts into readable text."""
    if not alerts:
        return "No active weather alerts for this area."

    formatted = []
    for alert in alerts:
        props = alert.get("properties", {})
        formatted.append(
            f"Event: {props.get('event', 'Unknown')}\n"
            f"Area: {props.get('areaDesc', 'Unknown')}\n"
            f"Severity: {props.get('severity', 'Unknown')}\n"
            f"Description: {props.get('description', 'No description')}\n"
            f"Instructions: {props.get('instruction', 'No specific instructions')}\n"
        )
    return "\n---\n".join(formatted)


# --- TOOLS ---

@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get active weather alerts for a US state.

    Args:
        state: Two-letter US state code (e.g., CA, NY, TX)
    """
    url = f"{NWS_API_BASE}/alerts/active?area={state.upper()}"
    data = await make_nws_request(url)

    if not data or "features" not in data:
        return "Unable to fetch alerts. Please check the state code and try again."

    return format_alerts(data["features"])


@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get the weather forecast for a specific location.

    Args:
        latitude: Latitude of the location (-90 to 90)
        longitude: Longitude of the location (-180 to 180)
    """
    point_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    point_data = await make_nws_request(point_url)

    if not point_data or "properties" not in point_data:
        return (
            f"Unable to fetch forecast data for coordinates "
            f"({latitude}, {longitude}). Ensure these are valid US coordinates."
        )

    forecast_url = point_data["properties"].get("forecast")
    if not forecast_url:
        return "Unable to determine forecast URL for this location."

    forecast_data = await make_nws_request(forecast_url)
    if not forecast_data or "properties" not in forecast_data:
        return "Unable to fetch the forecast. Please try again later."

    periods = forecast_data["properties"].get("periods", [])
    if not periods:
        return "No forecast periods available."

    formatted = []
    for period in periods[:5]:
        formatted.append(
            f"{period['name']}:\n"
            f"  Temperature: {period['temperature']}°{period['temperatureUnit']}\n"
            f"  Wind: {period['windSpeed']} {period['windDirection']}\n"
            f"  Forecast: {period['detailedForecast']}\n"
        )

    return "\n".join(formatted)


# --- RESOURCES ---

@mcp.resource("weather://info")
def get_weather_info() -> str:
    """Information about this weather server and its capabilities."""
    return """
    Weather Server v1.0
    ===================
    This server provides access to US weather data via the
    National Weather Service API.

    Available tools:
    - get_alerts: Get active weather alerts by US state code
    - get_forecast: Get weather forecast by latitude/longitude

    Data source: National Weather Service (weather.gov)
    Coverage: United States only
    """


# --- PROMPTS ---

@mcp.prompt()
def severe_weather_check(state: str) -> list[base.Message]:
    """Check for severe weather in a US state and provide safety recommendations."""
    return [
        base.UserMessage(
            content=f"Check for any active weather alerts in {state.upper()} "
            f"and provide a summary. If there are severe weather alerts, "
            f"include safety recommendations."
        )
    ]


# Entry point
if __name__ == "__main__":
    mcp.run()

Advanced Patterns and Best Practices

Once you have the basics working, here are patterns to level up your MCP server development.

Structured Tool Responses

Instead of returning plain strings, you can return structured content using the SDK's content types:

from mcp.types import TextContent, ImageContent

@mcp.tool()
async def get_weather_map(state: str) -> list[TextContent]:
    """Get weather information with structured output."""
    # ... fetch data ...
    return [
        TextContent(type="text", text=f"Weather data for {state}"),
        TextContent(type="text", text=formatted_data),
    ]

Environment Variables and Configuration

For servers that need API keys or configuration:

import os

@mcp.tool()
async def search_weather_history(
    location: str, date: str
) -> str:
    """Search historical weather data (requires API key)."""
    api_key = os.environ.get("WEATHER_API_KEY")
    if not api_key:
        return "Error: WEATHER_API_KEY environment variable not set."
    # ... use the API key ...

Configure environment variables in Claude Desktop:

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": ["--directory", "/path/to/server", "run", "server.py"],
      "env": {
        "WEATHER_API_KEY": "your-api-key-here"
      }
    }
  }
}

Error Handling Best Practices

Implement robust error handling so Claude receives useful information even when things go wrong:

@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get active weather alerts for a US state.

    Args:
        state: Two-letter US state code (e.g., CA, NY, TX)
    """
    # Validate input
    if len(state) != 2 or not state.isalpha():
        return (
            f"Invalid state code: '{state}'. "
            f"Please provide a two-letter US state code like 'CA' or 'NY'."
        )

    try:
        url = f"{NWS_API_BASE}/alerts/active?area={state.upper()}"
        data = await make_nws_request(url)

        if not data:
            return (
                f"The weather service returned no data for state '{state}'. "
                f"This might be a temporary issue. Please try again."
            )

        return format_alerts(data.get("features", []))

    except Exception as e:
        return f"An error occurred while fetching alerts: {str(e)}"

Lifespan Management for Persistent Connections

If your server needs to maintain database connections or other persistent resources, use the lifespan context manager:

from contextlib import asynccontextmanager

@asynccontextmanager
async def server_lifespan(server: FastMCP):
    """Manage server startup and shutdown."""
    # Startup: initialize resources
    db = await create_database_pool()
    server.state["db"] = db
    try:
        yield
    finally:
        # Shutdown: clean up resources
        await db.close()

mcp = FastMCP("My Server", lifespan=server_lifespan)

@mcp.tool()
async def query_data(sql: str) -> str:
    """Run a read-only SQL query."""
    db = mcp.state["db"]
    results = await db.fetch(sql)
    return str(results)

Common Mistakes and How to Avoid Them

Here are the most frequent issues developers encounter when building their first MCP server:

MistakeSymptomFix
Relative path in configServer not foundUse absolute paths in claude_desktop_config.json
Missing dependenciesImport errors at startupAdd all dependencies to pyproject.toml with uv add
Forgetting to restart ClaudeOld tools showingFully quit and reopen Claude Desktop
No docstring on toolClaude does not understand the toolAdd a descriptive docstring to every tool function
Synchronous I/O in async toolServer hangsUse async libraries (httpx, aiofiles) instead of sync ones
Print statementsServer crashes (stdout is for protocol)Use logging module or stderr for debug output

The stdout Trap

One of the most common pitfalls: never use print() in an MCP server. The stdio transport uses stdout for JSON-RPC messages. Any stray print statement will corrupt the protocol stream and crash the connection.

Instead, use Python's logging module:

import logging

logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
logger = logging.getLogger("weather-server")

@mcp.tool()
async def get_alerts(state: str) -> str:
    logger.info(f"Fetching alerts for state: {state}")
    # ...

Project Structure for Larger Servers

As your server grows beyond a single file, organize it into a proper package:

mcp-weather-server/
  pyproject.toml
  src/
    weather_server/
      __init__.py
      server.py          # FastMCP instance and entry point
      tools/
        __init__.py
        alerts.py         # Alert-related tools
        forecast.py       # Forecast-related tools
      resources/
        __init__.py
        info.py           # Resource definitions
      utils/
        __init__.py
        nws_client.py     # API client utilities

Then update your pyproject.toml to define the entry point:

[project.scripts]
weather-server = "weather_server.server:main"

What to Build Next

Now that you have a working MCP server, here are some ideas for expanding your skills:

  1. Database server -- Expose your PostgreSQL or SQLite database to Claude for natural language querying
  2. File system server -- Let Claude read and search through local project files
  3. API wrapper -- Turn any REST API into an MCP server (GitHub, Jira, Notion)
  4. Monitoring server -- Give Claude access to application logs and metrics

For building servers in other languages, see our guide on Building MCP Servers in Node.js, TypeScript & Go.

To understand the building blocks (tools, resources, prompts) at a deeper level, read MCP Core Building Blocks.

When you are ready to test more thoroughly, check out our Testing & Debugging MCP Servers guide.

Summary

Building an MCP server in Python follows a straightforward workflow:

  1. Set up your project with uv init and install the mcp[cli] SDK
  2. Define tools using @mcp.tool() with type hints and docstrings
  3. Add resources using @mcp.resource() for static data exposure
  4. Create prompts using @mcp.prompt() for reusable interaction templates
  5. Test with mcp dev server.py to launch the Inspector
  6. Configure Claude Desktop by editing claude_desktop_config.json
  7. Iterate by adding more tools and testing incrementally

The MCP Python SDK's FastMCP class handles all the protocol complexity -- JSON-RPC messaging, schema generation, transport management -- so you can focus on building the functionality that matters. Start simple, test often, and expand your server as your needs grow.

Browse our MCP Server Directory to see examples of production MCP servers and get inspiration for your next project.

Frequently Asked Questions

What Python version do I need to build an MCP server?

You need Python 3.10 or higher. The MCP Python SDK uses modern Python features including type hints and async/await patterns that require 3.10+. We recommend Python 3.11 or 3.12 for the best performance and compatibility.

What is the uv package manager and why is it recommended for MCP?

uv is a fast Python package manager written in Rust, created by Astral. The official MCP Python SDK documentation recommends uv because it handles virtual environments automatically, resolves dependencies faster than pip, and provides the uvx command for running tools like MCP Inspector without global installs.

Can I use pip instead of uv to install the MCP SDK?

Yes, you can install the MCP SDK with pip using 'pip install mcp[cli]'. However, uv is recommended because it manages virtual environments automatically and provides the uvx command used for running MCP Inspector. If you use pip, you will need to manage your virtual environment manually.

What is FastMCP and how does it relate to the MCP SDK?

FastMCP is the high-level Python interface included in the official MCP Python SDK (mcp package). It provides a decorator-based API similar to FastAPI that makes it easy to define tools, resources, and prompts. It handles all the protocol details, JSON-RPC messaging, and transport management automatically.

How do I test my MCP server without connecting it to Claude?

Use the MCP Inspector, which is a web-based testing tool included with the SDK. Run 'mcp dev your_server.py' to launch it. The Inspector lets you call tools, read resources, and test prompts interactively through a browser interface at localhost:5173.

Why is my MCP server not showing up in Claude Desktop?

Common causes include: incorrect path in claude_desktop_config.json, using a relative path instead of an absolute path, the server file having syntax errors, missing dependencies in the environment, or not restarting Claude Desktop after changing the configuration. Check the Claude Desktop developer logs for specific error messages.

Can my MCP server make network requests to external APIs?

Yes, MCP servers can make any network requests. This is one of the primary use cases — wrapping external APIs as MCP tools so AI assistants can interact with them. Use libraries like httpx (async) or requests in your tool implementations. Just ensure you handle errors gracefully and implement appropriate timeouts.

What is the difference between MCP tools, resources, and prompts?

Tools are functions the AI model can call to perform actions (like querying an API). Resources are data endpoints the application can read (like files or database records). Prompts are reusable templates that structure how the AI interacts with your server. Tools are model-controlled, resources are application-controlled, and prompts are user-controlled.

How do I handle errors in my MCP server tools?

Return errors by raising exceptions or returning error information in your tool response. The MCP SDK will automatically convert Python exceptions into proper MCP error responses. For expected errors (like 'city not found'), return a descriptive text message. For unexpected errors, let them propagate and the SDK will return an internal error to the client.

Can I build an MCP server that connects to a database?

Yes, database access is one of the most common MCP server use cases. You can use any Python database library (SQLAlchemy, asyncpg, pymongo, etc.) in your tool implementations. Create tools for querying data and resources for exposing schema information. See our guide on database MCP servers for detailed examples.

Related Articles

Related Guides