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

Learn to build a Model Context Protocol (MCP) server from scratch. This practical tutorial shows you how to create tools for AI assistants like Claude to access external APIs.

Canonical URL: https://a2a-mcp.org/blog/how-to-build-mcp-server

Language: en

Published: 2026-04-06

Updated: 2026-04-06

## 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](https://docs.astral.sh/uv/getting-started/installation/) (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:

```bash
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:

```bash
touch weather.py
```

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

```python
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:

```bash
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):

```json
{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "--directory",
        "/ABSOLUTE/PATH/TO/weather-mcp-server",
        "run",
        "weather.py"
      ]
    }
  }
}
```

3. Fully quit Claude Desktop (Cmd+Q on macOS or close from system tray on Windows) and restart it.
4. 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!
