← Back to MCP Tutorials

How to Build an MCP Server in Python

Step-by-step guide to creating your first MCP server. Connect Claude and other LLMs to custom tools in under 30 minutes.

February 3, 2026·12 min read

MCP (Model Context Protocol) lets AI models interact with external tools and data. Instead of just generating text, an LLM can read files, query databases, call APIs, and take real actions.

In this tutorial, you'll build an MCP server in Python that gives AI models the ability to check the weather. By the end, you'll understand the MCP architecture well enough to build any tool integration you need.

What You'll Learn

  • How MCP servers work (architecture overview)
  • Setting up a Python MCP server with FastMCP
  • Defining tools that AI can call
  • Testing your server locally
  • Connecting to Claude Desktop

Prerequisites

  • Python 3.10 or higher
  • Basic familiarity with Python
  • Claude Desktop (for testing) or any MCP client

How MCP Works

Before we write code, let's understand the architecture:

┌─────────────┐     MCP Protocol      ┌─────────────┐
│   Claude    │ ◄──────────────────► │  MCP Server │
│  (Client)   │    JSON-RPC over     │  (Your Code)│
└─────────────┘       stdio          └─────────────┘
                                            │
                                            ▼
                                     ┌─────────────┐
                                     │ External    │
                                     │ Services    │
                                     └─────────────┘

The MCP client (Claude) sends requests to your server. Your server exposes toolsthat the client can call. When Claude needs to perform an action, it calls your tool and uses the result.

Step 1: Set Up Your Project

Create a new directory and set up a virtual environment:

mkdir my-mcp-server
cd my-mcp-server
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

Install FastMCP (the easiest way to build MCP servers in Python):

pip install fastmcp

Step 2: Create Your First Server

Create a file called server.py:

from fastmcp import FastMCP

# Create the server
mcp = FastMCP("Weather Server")


@mcp.tool()
def get_weather(city: str) -> str:
    """Get the current weather for a city.
    
    Args:
        city: The name of the city to check weather for.
    
    Returns:
        A string describing the current weather.
    """
    # In a real implementation, you'd call a weather API
    # For this example, we'll return mock data
    weather_data = {
        "new york": "72°F, Partly Cloudy",
        "london": "55°F, Rainy",
        "tokyo": "68°F, Clear",
        "paris": "61°F, Overcast",
    }
    
    city_lower = city.lower()
    if city_lower in weather_data:
        return f"Weather in {city}: {weather_data[city_lower]}"
    return f"Weather data not available for {city}"


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

That's it! You've just created an MCP server with one tool. Let's break down what's happening:

  • FastMCP("Weather Server") — Creates a new MCP server with a name
  • @mcp.tool() — Decorator that registers a function as a tool
  • The docstring becomes the tool description (AI sees this to understand what the tool does)
  • Type hints (city: str) define the parameter types
  • mcp.run() — Starts the server

Step 3: Test Your Server

You can test the server using the MCP inspector:

npx @modelcontextprotocol/inspector python server.py

This opens a web interface where you can see your tools and test them manually. You should see your get_weather tool listed.

Step 4: Connect to Claude Desktop

To use your server with Claude Desktop, add it to your Claude configuration:

On macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

On Windows: %APPDATA%\\Claude\\claude_desktop_config.json

{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["/full/path/to/your/server.py"]
    }
  }
}

Restart Claude Desktop. You should now be able to ask Claude about the weather, and it will use your tool to respond.

Step 5: Add a Real API

Let's upgrade the server to use a real weather API. We'll use wttr.in, which doesn't require an API key:

import httpx
from fastmcp import FastMCP

mcp = FastMCP("Weather Server")


@mcp.tool()
async def get_weather(city: str) -> str:
    """Get the current weather for a city.
    
    Args:
        city: The name of the city to check weather for.
    
    Returns:
        Current weather conditions including temperature and description.
    """
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://wttr.in/{city}?format=%C+%t",
            headers={"User-Agent": "curl"},
            timeout=10.0,
        )
        
        if response.status_code == 200:
            return f"Weather in {city}: {response.text.strip()}"
        return f"Could not fetch weather for {city}"


@mcp.tool()
async def get_forecast(city: str, days: int = 3) -> str:
    """Get a multi-day weather forecast for a city.
    
    Args:
        city: The name of the city.
        days: Number of days to forecast (1-3). Defaults to 3.
    
    Returns:
        Weather forecast for the specified number of days.
    """
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://wttr.in/{city}?format=j1",
            headers={"User-Agent": "curl"},
            timeout=10.0,
        )
        
        if response.status_code != 200:
            return f"Could not fetch forecast for {city}"
        
        data = response.json()
        forecasts = []
        
        for day in data.get("weather", [])[:days]:
            date = day.get("date", "Unknown")
            max_temp = day.get("maxtempF", "?")
            min_temp = day.get("mintempF", "?")
            desc = day.get("hourly", [{}])[4].get("weatherDesc", [{}])[0].get("value", "Unknown")
            forecasts.append(f"{date}: {min_temp}°F - {max_temp}°F, {desc}")
        
        return f"Forecast for {city}:\n" + "\n".join(forecasts)


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

Don't forget to install httpx:

pip install httpx

Adding Resources

MCP servers can also expose resources — data that AI can read. Here's how to add a resource:

@mcp.resource("weather://supported-cities")
def list_supported_cities() -> str:
    """List of cities with guaranteed weather data."""
    cities = [
        "New York", "London", "Tokyo", "Paris",
        "Sydney", "Berlin", "Toronto", "Mumbai"
    ]
    return "\n".join(cities)

Resources are useful for providing context that the AI can reference without making a tool call.

Error Handling

Good MCP servers handle errors gracefully. Here's the pattern:

@mcp.tool()
async def get_weather(city: str) -> str:
    """Get weather for a city."""
    try:
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"https://wttr.in/{city}?format=%C+%t",
                headers={"User-Agent": "curl"},
                timeout=10.0,
            )
            response.raise_for_status()
            return f"Weather in {city}: {response.text.strip()}"
    except httpx.TimeoutException:
        return f"Error: Request timed out while fetching weather for {city}"
    except httpx.HTTPStatusError as e:
        return f"Error: Weather service returned status {e.response.status_code}"
    except Exception as e:
        return f"Error: Could not fetch weather - {str(e)}"

Next Steps

You now have a working MCP server! Here are some ideas to extend it:

  • Add more tools (database queries, file operations, API integrations)
  • Add resources for static data the AI should have access to
  • Implement proper authentication if your tools access sensitive data
  • Package your server for others to use

📚 Related Tutorials

  • MCP vs OpenAI Function Calling (coming soon)
  • Building an MCP Server in TypeScript (coming soon)
  • Advanced MCP: Resources and Prompts (coming soon)

Full Code

Here's the complete server code:

"""
Weather MCP Server
A simple MCP server that provides weather information.
"""
import httpx
from fastmcp import FastMCP

mcp = FastMCP("Weather Server")


@mcp.tool()
async def get_weather(city: str) -> str:
    """Get the current weather for a city.
    
    Args:
        city: The name of the city to check weather for.
    
    Returns:
        Current weather conditions including temperature and description.
    """
    try:
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"https://wttr.in/{city}?format=%C+%t",
                headers={"User-Agent": "curl"},
                timeout=10.0,
            )
            response.raise_for_status()
            return f"Weather in {city}: {response.text.strip()}"
    except Exception as e:
        return f"Error fetching weather: {str(e)}"


@mcp.tool()
async def get_forecast(city: str, days: int = 3) -> str:
    """Get a multi-day weather forecast.
    
    Args:
        city: The name of the city.
        days: Number of days (1-3). Defaults to 3.
    
    Returns:
        Weather forecast for the specified days.
    """
    try:
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"https://wttr.in/{city}?format=j1",
                headers={"User-Agent": "curl"},
                timeout=10.0,
            )
            response.raise_for_status()
            data = response.json()
            
            forecasts = []
            for day in data.get("weather", [])[:days]:
                date = day.get("date", "Unknown")
                max_temp = day.get("maxtempF", "?")
                min_temp = day.get("mintempF", "?")
                desc = day.get("hourly", [{}])[4].get("weatherDesc", [{}])[0].get("value", "Unknown")
                forecasts.append(f"{date}: {min_temp}°F - {max_temp}°F, {desc}")
            
            return f"Forecast for {city}:\n" + "\n".join(forecasts)
    except Exception as e:
        return f"Error fetching forecast: {str(e)}"


@mcp.resource("weather://supported-cities")
def list_supported_cities() -> str:
    """List of major cities with reliable weather data."""
    return """Major cities supported:
- New York, USA
- London, UK
- Tokyo, Japan
- Paris, France
- Sydney, Australia
- Berlin, Germany
- Toronto, Canada
- Mumbai, India

Note: Most cities worldwide are supported via wttr.in"""


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

Questions? Reach out on Twitter or email kai@kaigritun.com.

Get updates in your inbox

Tutorials, updates, and best practices for Model Context Protocol.

No spam. Unsubscribe anytime.