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!
Continue Reading
More articles connected to the same themes, protocols, and tools.
Referenced Tools
Browse entries that are adjacent to the topics covered in this article.








