Creating Your First MCP Server: A Step by Step Guide

August 20, 2025 by stein ove helset

MCP servers not only make it possible to get new real time data for your AI client, but it also makes the results more reliable, almost like you give it superpowers. In this article, I will go through how you can build your own first MCP (Model Context Protocol) server .

I will go through everything step by step, and break it all down as we go along. We’ll start from scratch by installing everything we need, and then we’ll continue by connecting to a weather service that fetches real time weather data. At the end of this article, we will have a working MCP server that can be used together with Claude and other MCP-compatible AI clients.

What Are We Building?

First, I just want to go through a little bit more in detail what we’re actually building. To check the weather right now, you probably go to Google and search, or you have some sort of weather service you go to and find your location. What I want to do now is to give the AI the “possibility” of looking out the window at any given location.

I’m very familiar with Python, so that’s what we’re going to focus on in this article. It’s beginner-friendly, so if you’re not familiar with Python, you still might be able to follow along.

Before We Start

You’ll need a few things set up:

  • Python 3.10 or newer (I’m using 3.12, and I’d recommend the same)
  • A text editor (Cursor, VS Code, PyCharm, or even Notepad++ will work)
  • Claude Desktop app (for testing our server later)

Setting Up Your Project

First, let’s create a proper environment for our project. I’m going to use uv which is a modern Python package manager, but you can use pip and venv if you prefer.

Option 1: Using uv (Recommended)

bash

# Install uv if you haven't already
pip install uv

# Create a new project
uv init weather-mcp-server
cd weather-mcp-server

# Add the MCP dependency
uv add mcp

Option 2: Using traditional pip/venv

bash

# Create and navigate to project directory
mkdir weather-mcp-server
cd weather-mcp-server

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install MCP
pip install mcp

Building the Server: Step by Step

Now comes the fun part. We’re going to build our weather server piece by piece, and I’ll explain what each part does.

Create a file called server.py and let’s start coding:

Step 1: The Basic Structure

python

#!/usr/bin/env python3
import asyncio
import httpx
from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
import mcp.server.stdio

# This is like the "business card" of our server
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "WeatherMCPServer/1.0"

# Create the server instance
server = Server("weather-server")

Think of the server variable as the foundation of our entire MCP server. Everything we build will be attached to this.

Step 2: Helper Functions

Before we create the actual tools, we need some helper functions. These are like the behind-the-scenes workers:

python

async def make_nws_request(url: str) -> dict:
    """
    Makes a request to the National Weather Service API.
    This is our universal way to talk to the weather API.
    """
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/geo+json"
    }
    
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers)
            response.raise_for_status()
            return response.json()
        except httpx.RequestError as e:
            # If something goes wrong, we return an empty dict
            return {}

def format_alert(feature: dict) -> str:
    """
    Takes the messy weather alert data and makes it human-readable.
    This is where we clean up the data for Claude to understand.
    """
    props = feature.get("properties", {})
    return f"""
Event: {props.get('event', 'Unknown')}
Area: {props.get('areaDesc', 'Unknown')}  
Severity: {props.get('severity', 'Unknown')}
Description: {props.get('description', 'No description available')}
Instructions: {props.get('instruction', 'No specific instructions')}
"""

These functions handle the “boring” stuff like making HTTP requests and formatting data. This way, our main tools can focus on the important logic.

Step 3: Creating the Tools

Now for the exciting part – the actual tools that Claude will be able to use:

python

@server.tool()
async def get_alerts(state: str) -> str:
    """
    Get weather alerts for a US state.
    
    Args:
        state: Two-letter US state code (e.g. CA, NY, TX)
    """
    # Build the API URL
    url = f"{NWS_API_BASE}/alerts/active/area/{state}"
    
    # Get the data
    data = await make_nws_request(url)
    
    # Handle the case where we get no data
    if not data or "features" not in data:
        return "Unable to fetch alerts or no alerts found."
    
    # Handle the case where there are no active alerts
    if not data["features"]:
        return "No active alerts for this state."
    
    # Format each alert and join them together
    alerts = [format_alert(feature) for feature in data["features"]]
    return "\n---\n".join(alerts)


@server.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """
    Get weather forecast for a location.
    
    Args:
        latitude: Latitude of the location
        longitude: Longitude of the location  
    """
    # The National Weather Service API is a bit quirky
    # We need to first get the "grid point" for our coordinates
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)
    
    if not points_data:
        return "Unable to fetch forecast data for this location."
    
    # Now we can get the actual forecast URL from the points response
    forecast_url = points_data["properties"]["forecast"]
    forecast_data = await make_nws_request(forecast_url)
    
    if not forecast_data:
        return "Unable to fetch detailed forecast."
    
    # Format the forecast periods (we'll show the next 5)
    periods = forecast_data["properties"]["periods"]
    forecasts = []
    
    for period in periods[:5]:  # Just the next 5 periods
        forecast = f"""
{period['name']}:
Temperature: {period['temperature']}°{period['temperatureUnit']}
Wind: {period['windSpeed']} {period['windDirection']}
Forecast: {period['detailedForecast']}
"""
        forecasts.append(forecast)
    
    return "\n---\n".join(forecasts)

The @server.tool() decorator is the magic that tells MCP “hey, Claude can call this function!” The docstrings are super important because they tell Claude what each tool does and how to use it.

Step 4: Running the Server

Finally, we need a way to actually run our server:

python

async def main():
    # MCP servers communicate through "standard input/output"
    # This is like having a conversation through text messages
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="weather-server",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )

# This is what actually starts everything
if __name__ == "__main__":
    asyncio.run(main())

The Complete Server Code

Here’s our complete server.py file all put together:

python

#!/usr/bin/env python3
import asyncio
import httpx
from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
import mcp.server.stdio

NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "WeatherMCPServer/1.0"

server = Server("weather-server")

async def make_nws_request(url: str) -> dict:
    """Makes 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)
            response.raise_for_status()
            return response.json()
        except httpx.RequestError as e:
            return {}

def format_alert(feature: dict) -> str:
    """Format weather alert data into readable text."""
    props = feature.get("properties", {})
    return f"""
Event: {props.get('event', 'Unknown')}
Area: {props.get('areaDesc', 'Unknown')}  
Severity: {props.get('severity', 'Unknown')}
Description: {props.get('description', 'No description available')}
Instructions: {props.get('instruction', 'No specific instructions')}
"""

@server.tool()
async def get_alerts(state: str) -> str:
    """
    Get 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}"
    data = await make_nws_request(url)
    
    if not data or "features" not in data:
        return "Unable to fetch alerts or no alerts found."
    
    if not data["features"]:
        return "No active alerts for this state."
    
    alerts = [format_alert(feature) for feature in data["features"]]
    return "\n---\n".join(alerts)

@server.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """
    Get weather forecast for a location.
    
    Args:
        latitude: Latitude of the location
        longitude: Longitude of the location  
    """
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)
    
    if not points_data:
        return "Unable to fetch forecast data for this location."
    
    forecast_url = points_data["properties"]["forecast"]
    forecast_data = await make_nws_request(forecast_url)
    
    if not forecast_data:
        return "Unable to fetch detailed forecast."
    
    periods = forecast_data["properties"]["periods"]
    forecasts = []
    
    for period in periods[:5]:
        forecast = f"""
{period['name']}:
Temperature: {period['temperature']}°{period['temperatureUnit']}
Wind: {period['windSpeed']} {period['windDirection']}
Forecast: {period['detailedForecast']}
"""
        forecasts.append(forecast)
    
    return "\n---\n".join(forecasts)

async def main():
    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="weather-server",
                server_version="0.1.0",
                capabilities=server.get_capabilities(
                    notification_options=NotificationOptions(),
                    experimental_capabilities={},
                ),
            ),
        )

if __name__ == "__main__":
    asyncio.run(main())

Testing Your Server

Before we connect it to Claude, let’s make sure it works. You can test it from the command line:

bash

# If using uv:
uv run server.py

# If using pip/venv:
python server.py

If everything is working, the server will start and wait for input. You won’t see much output, which is normal – MCP servers are designed to communicate through structured messages.

Connecting to Claude Desktop

Now comes the exciting part – connecting your server to Claude Desktop so you can actually use it!

Step 1: Find Your Configuration File

You need to edit Claude Desktop’s configuration file. The location depends on your operating system:

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

If the file doesn’t exist, create it.

Step 2: Configure Your Server

Open the configuration file and add your server. Here’s what it should look like:

json

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

Important: Replace /full/path/to/your/weather-mcp-server/ with the actual path to your project folder.

If you’re using regular pip/venv instead of uv, your configuration would be:

json

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

Step 3: Restart Claude Desktop

Close Claude Desktop completely and restart it. You should see a small hammer icon (🔨) appear in the interface, indicating that MCP tools are available.

Testing Your Weather Server

Now for the moment of truth! Open Claude Desktop and try these queries:

  • “What’s the weather forecast for San Francisco?” (you might need to provide coordinates like 37.7749, -122.4194)
  • “Are there any weather alerts for California?” (use “CA”)
  • “Get me weather alerts for Texas” (use “TX”)

If everything worked, Claude should be able to fetch real-time weather data and present it to you in a nice, readable format.

What Just Happened?

Let’s take a step back and understand what we built:

  1. We created tools that can fetch real-time data from the internet
  2. We packaged them as an MCP server that speaks the MCP protocol
  3. We connected Claude to our server so it can use our tools
  4. Claude can now get live weather data whenever you ask for it

This is the power of MCP – instead of Claude being limited to its training data, it can now access real-time information through your custom server.

Where to Go From Here

Congratulations! You’ve just built your first MCP server. But this is just the beginning. Here are some ideas for expanding your weather server:

  • Add more locations: The National Weather Service works great for the US, but you could add other weather APIs for global coverage
  • Add more data types: How about radar images, satellite data, or historical weather?
  • Add caching: Store recent requests to make your server faster
  • Add error handling: Make your server more robust with better error messages

You could also build completely different types of MCP servers:

  • File system access: Let Claude read and write files on your computer
  • Database connections: Connect Claude to your databases
  • API integrations: Connect to your favorite services like GitHub, Slack, or Google Drive
  • Custom business logic: Implement your company’s specific workflows

Understanding the Big Picture

What we’ve built here is more than just a weather app. We’ve created a bridge between Claude and the real world. This is what makes MCP so powerful – it’s not just about one specific integration, it’s about creating a standard way for AI assistants to connect to any system or service.

Every MCP server you build follows this same pattern:

  1. Define tools with clear descriptions
  2. Implement the logic for each tool
  3. Set up the server to communicate via MCP protocol
  4. Configure your AI client to use the server

Once you understand this pattern, you can build MCP servers for virtually anything.

Resources for Learning More

Ready to dive deeper? Here are some great resources:

The MCP community is growing fast, and there are new servers being built all the time. Don’t be surprised if you find yourself building multiple MCP servers – it’s pretty addictive once you see how powerful they can be!

Remember, the goal isn’t to become an MCP expert overnight. Start with simple projects like this weather server, get comfortable with the concepts, and gradually build more complex integrations. Before you know it, you’ll be building MCP servers that solve real problems in your daily workflow.

Happy coding, and welcome to the world of MCP!

Related Articles