A Practical Guide: Building Your First MCP Server in Python in Under 30 Minutes
A step-by-step practical guide to building your first MCP server in Python with FastMCP: from environment setup to tools, resources, Inspector testing, and connecting to Claude Desktop, with real code.
Imagine you want Claude or Cursor to read your local files, query your database, or run a function in your system — not by pasting text manually, but by having the model "discover" your tools and call them on its own when needed. This is exactly what the Model Context Protocol, or MCP, enables; Anthropic released it in late 2024 and within months it became a de facto standard for connecting AI agents to the real world. In this guide we'll build a first MCP server in Python in under half an hour, step by step, with real code.
What Is an MCP Server, Really?
Before the code, let's set the right mental model. An MCP server is a small program that "exposes" three kinds of capabilities to any compatible AI client: Tools, which are functions the model executes like POST endpoints; Resources, read-only data like GET endpoints; and Prompts, ready-made templates for recurring tasks. The direction matters: you do not call the model from inside the server; rather, the client (like Claude Desktop) connects to your server, asks it "what tools do you have?", then calls them when the model decides they are useful. Your server is a passive party that waits and responds, nothing more.
Why FastMCP?
You can deal with the MCP protocol via Anthropic's official package directly, but that means hand-writing tool schemas and dealing with JSON-RPC details. Here comes the FastMCP framework, which now powers about 70% of MCP servers across all languages, and with it all this complexity collapses into a single decorator. You write a regular Python function with type hints and a description, put one line above it, and FastMCP generates the schema, validates inputs, and surfaces the description to the model automatically. Note: FastMCP 1.0 is built into the official package, while the newer versions (3.x in 2026) evolve as a standalone project with extra features.
Step 1: Setting Up the Environment
We'll use the modern uv tool to manage the project, as it is faster and cleaner. Create the project, activate the environment, and install the package:
uv init weather-mcp
cd weather-mcp
uv venv
source .venv/bin/activate
uv add "mcp[cli]" httpx
The mcp package is the official SDK (and includes FastMCP), httpx is for network calls, and the cli extra brings the Inspector tool for testing. Create a file named server.py and begin.
Step 2: Your First Tool
Here is a complete, working server with a single tool that adds two numbers. Note the simplicity of the decorator:
from mcp.server.fastmcp import FastMCP
# Create the server
mcp = FastMCP("weather")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two integers and return the result."""
return a + b
if __name__ == "__main__":
mcp.run()
That is all for a server that actually works. The description in the triple-quoted docstring is not decoration; the model reads it to know when to call the tool, so write it carefully. And the type hints (int) automatically turn into input validation rules.
Step 3: A Real Tool That Calls an External API
Addition is a teaching exercise; let's build a useful tool that fetches weather from a real API. Note the use of async for network operations, and error handling so the server does not crash when the request fails:
import httpx
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
NWS_API = "https://api.weather.gov"
@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
"""Fetch the weather forecast for given coordinates (latitude and longitude)."""
url = f"{NWS_API}/points/{latitude},{longitude}"
async with httpx.AsyncClient() as client:
try:
resp = await client.get(url, timeout=10.0)
resp.raise_for_status()
data = resp.json()
return f"Area data: {data['properties']['forecast']}"
except Exception as e:
return f"Could not fetch weather: {e}"
if __name__ == "__main__":
mcp.run()
An important principle: wrap every external call in error handling that returns a clear message instead of throwing an exception that takes down the entire server. The model treats the message as a result, so it gently tells the user the operation failed.
Step 4: Adding a Resource
Resources are read-only and are defined with a URI pattern. An example exposing app settings:
import json
@mcp.resource("config://app/settings")
def app_settings() -> str:
"""Current app settings as JSON."""
return json.dumps({
"version": "1.0.0",
"language": "ar",
"max_results": 50,
})
A resource can also be dynamic with parameters inside the URI, like user://{user_id}/profile, which receives user_id and returns its data. The core difference from a tool: a resource loads information into the model's context and does not produce a side effect.
Step 5: Testing the Server Before Connecting
Do not connect a server you have not tested. The MCP Inspector gives you a web interface to try your tools and resources manually. Run it with a single command:
uv run mcp dev server.py
A local interface will open where you see the list of your tools, and you can call each one with test inputs and watch the response. This step catches most errors before they reach the actual client.
Step 6: Connecting to Claude Desktop
To connect your server to Claude Desktop, edit its config file and add your server under the mcpServers key, with the absolute path to your project folder:
{
"mcpServers": {
"weather": {
"command": "uv",
"args": [
"--directory",
"/ABSOLUTE/PATH/TO/weather-mcp",
"run",
"server.py"
]
}
}
}
Get the absolute path with the pwd command, and the uv path with which uv if needed. Restart Claude Desktop, and your tools will appear ready to be called.
A Common Pitfall That Crashes Servers
If your server runs over the standard transport (stdio), never print to standard output (stdout) via a regular print, because that corrupts the JSON-RPC messages and silently breaks the server. Use logging to standard error (stderr) instead. This mistake is among the most baffling for beginners, because the server "works" on the surface but fails to communicate.
Where to Go Next?
You have now built a server that exposes tools and resources, tested it, and connected it to a real client — all in dozens of lines. The natural next step is moving it from a local prototype to a service via Streamable HTTP transport when more than one agent needs it, and adding OAuth authentication if you handle sensitive data. But the most important practical advice: do not over-build from day one. Start with one solid tool over stdio, validate it in the Inspector, then promote it to an authenticated service only when you actually need it. Most teams build more than they need in their first server.
Was this article helpful?