Back to Blog
BlogApril 6, 20263

How to Build Your First MCP Server: Step-by-Step Tutorial

How to Build Your First MCP Server: Step-by-Step Tutorial

Prerequisites

Before you start, make sure you have these ready:

  • Python 3.10 or higher installed.
  • Basic familiarity with Python functions and async code.
  • uv (fast Python package manager) – install with curl -LsSf https://astral.sh/uv/install.sh | sh.
  • An MCP client such as Claude Desktop (download from claude.ai/download and keep it updated).
  • A text editor or IDE.

No prior MCP experience is needed. We'll build a practical weather MCP server that lets AI query real-time U.S. weather alerts and forecasts via the National Weather Service API.

Step 1: Set Up the Environment

Create a clean project directory and initialize it with uv:

mkdir weather-mcp-server
cd weather-mcp-server
uv init
uv venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
uv add "mcp[cli]" httpx

This installs the official MCP Python SDK (FastMCP) and httpx for API calls. Your project now has a pyproject.toml and virtual environment.

Expected output: A new .venv folder and dependencies listed in uv.lock.

Step 2: Create the MCP Server Code

Create the main file:

touch weather.py

Paste this complete, ready-to-run code into weather.py:

from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP

# Initialize the MCP server
mcp = FastMCP("weather")

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

# Helper function to call the NWS API safely
async def make_nws_request(url: str) -> dict[str, Any] | None:
    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 Exception:
            return None

# Format weather alerts nicely for the AI
def format_alert(feature: dict) -> str:
    props = feature["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 provided")}
"""

# Tool 1: Get active weather alerts for a U.S. state
@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get current weather alerts for a U.S. state (e.g., "CA" or "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 or no alerts found."
    if not data["features"]:
        return f"No active alerts for {state.upper()}."
    alerts = [format_alert(f) for f in data["features"]]
    return "\n---\n".join(alerts)

# Tool 2: Get 5-day forecast for any lat/long
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get weather forecast for a specific latitude and longitude."""
    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"][:5]
    forecasts = []
    for period in 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)

def main():
    mcp.run(transport="stdio")

if __name__ == "__main__":
    main()

Key concepts explained:

  • @mcp.tool() decorators turn regular async functions into MCP tools that AI can discover and call.
  • The server runs over stdio (standard input/output) – the default for local MCP servers.
  • All logging should go to stderr (FastMCP handles this automatically).

Step 3: Test the Server Locally

Run the server:

uv run weather.py

Expected output: The terminal stays open and silent (this is normal for stdio servers). You’ll see JSON-RPC messages only when an MCP client connects.

Leave this terminal running for the next step.

Step 4: Connect the MCP Server to Claude Desktop

  1. Open Claude Desktop.
  2. Create or edit the config file at:
    • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
    • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add this exact entry (replace /ABSOLUTE/PATH/TO/weather-mcp-server with your real folder path):

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "--directory",
        "/ABSOLUTE/PATH/TO/weather-mcp-server",
        "run",
        "weather.py"
      ]
    }
  }
}
  1. Fully quit Claude Desktop (Cmd+Q on macOS or close from system tray on Windows) and restart it.
  2. In Claude, click “Add files, connectors, and more” → hover over Connectors → you should see “weather” listed.

Step 5: Use Your MCP Server with AI

Try these prompts in Claude Desktop:

  • “What are the active weather alerts in Texas?”
  • “Give me the forecast for San Francisco (use lat 37.77, long -122.41).”
  • “Check alerts in California and tell me if I need to prepare for anything.”

Claude will automatically discover the tools, ask for your approval the first time, and return formatted results.

Expected behavior: The AI calls your tools behind the scenes and shows natural-language answers.

Common Issues & Troubleshooting

  • Server doesn’t appear in Claude:

    • Double-check the JSON is valid (no trailing commas).
    • Use an absolute path in the config.
    • Restart Claude completely.
    • Check logs: ~/Library/Logs/Claude/mcp*.log (macOS) or equivalent on Windows.
  • API errors or no data:

    • NWS API only works for U.S. locations.
    • Use two-letter state codes (CA, TX, etc.).
    • Coordinates must be valid lat/long.
  • “Command not found”:

    • Make sure uv is in your PATH and the virtual environment is activated.
    • Run uv --version to verify installation.
  • Timeout or slow response:

    • Increase timeout in make_nws_request if needed.
    • NWS has rate limits – avoid spamming in production.
  • Permission issues:

    • On macOS, grant Claude Desktop full disk access in System Settings → Privacy & Security.

Next Steps

  • Add more tools: Create tools for databases, GitHub, Slack, or your own APIs using the same @mcp.tool() pattern.
  • Add resources and prompts: Use mcp.resource() and mcp.prompt() for file-like data and reusable instructions.
  • Deploy remotely: Switch to HTTP/SSE transport and host on AWS Lambda, Vercel, or any server (FastMCP supports stateless_http=True).
  • Support multiple languages: Try the official TypeScript, Go, or Rust SDKs for the same functionality.
  • Share your server: Publish the repo so others can add it via npx or Docker.

You now have a fully functional MCP server that any compatible AI client can use. Experiment, extend the tools, and start building powerful AI integrations today!

Share this article