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
- Open Claude Desktop.
- Create or edit the config file at:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
- macOS:
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"
]
}
}
}
- Fully quit Claude Desktop (Cmd+Q on macOS or close from system tray on Windows) and restart it.
- 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 --versionto verify installation.
-
Timeout or slow response:
- Increase timeout in
make_nws_requestif needed. - NWS has rate limits – avoid spamming in production.
- Increase timeout in
-
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()andmcp.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
npxor 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!