Back to Blog
BlogApril 6, 2026508

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

Referenced Tools

Browse entries that are adjacent to the topics covered in this article.

Explore directory