MCP (Model Context Protocol) gives AI models a structured way to call your code. You define tools on a server. The model discovers them, picks when to use them, and sends structured input. The Python SDK makes this straightforward with decorator-based tool registration and a built-in CLI for testing and installation.
This guide walks you through building a working MCP server in Python, registering a tool, and wiring it into Claude Desktop. No boilerplate, no framework overhead.
Prerequisites
- Python 3.10 or later
- uv (fast Python package manager, recommended by the MCP team)
- A code editor
- Claude Desktop installed (for testing)
Check your Python version:
python3 --version
Install uv if you do not have it:
curl -LsSf https://astral.sh/uv/install.sh | sh
1. Project Setup
Create a new directory and initialize a Python project:
mkdir mcp-weather && cd mcp-weather
uv init
Add the MCP SDK as a dependency:
uv add "mcp[cli]"
The [cli] extra includes the mcp command-line tools you will use for testing and installation later.
2. Build the Server
Create a file called server.py at the project root:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
def get_weather(city: str) -> str:
"""Get current weather for a given city. Returns mock data."""
import random
conditions = ["sunny", "cloudy", "rainy", "windy", "snowy"]
condition = random.choice(conditions)
temp_c = random.randint(5, 35)
temp_f = round(temp_c * 9 / 5 + 32)
return f"Weather in {city}: {condition}, {temp_c}C ({temp_f}F)"
if __name__ == "__main__":
mcp.run()
That is the entire server. A few things worth noting:
FastMCPis the high-level server class. You pass it a name and it handles protocol negotiation, tool registration, and transport setup.- The
@mcp.tool()decorator registers any function as an MCP tool. The function signature becomes the input schema automatically. Type hints matter here. - The docstring becomes the tool description that the model sees when deciding whether to call the tool.
mcp.run()starts the server on stdio transport by default, which is what Claude Desktop expects for local connections.
3. Test It Locally
The SDK ships with a built-in inspector that lets you test your server without connecting it to any client. Run:
uv run mcp dev server.py
This opens an interactive inspector in your browser where you can see registered tools, send test calls, and inspect the protocol messages going back and forth. Try calling get_weather with {"city": "Tokyo"} to verify the output looks right.
You can also run the server directly to confirm it starts without errors:
uv run python server.py
The process will hang waiting for MCP protocol messages on stdin. That is expected. Kill it with Ctrl+C.
4. Wire It into Claude Desktop
The SDK has an install command that writes the config for you:
uv run mcp install server.py
This updates Claude Desktop’s config file (~/Library/Application Support/Claude/claude_desktop_config.json on macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows) to point at your server.
You can also set a custom name:
uv run mcp install server.py --name "Weather Server"
If your server needs environment variables (API keys, database URLs), pass them at install time:
uv run mcp install server.py -v API_KEY=your_key_here
Manual config (if you prefer). Open the Claude Desktop config file and add your server entry. Replace the path with your actual project location:
{
"mcpServers": {
"weather-server": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/mcp-weather", "python", "server.py"]
}
}
}
The path must be absolute. Relative paths will not resolve correctly.
5. Verify in Claude Desktop
Restart Claude Desktop after changing the config. Open a new conversation and ask something like:
What is the weather in Berlin?
Claude will discover your get_weather tool, call it, and return the mock data. You should see an indicator that Claude is using the “weather-server” tool before responding.
If the tool does not appear, check:
- You ran
uv run mcp install server.pyor manually added the config entry - The path in the config is absolute and points to the right directory
uvis available in your system PATH- You restarted Claude Desktop after editing the config
What to Build Next
You have a working server with one mock tool. Here is where to take it.
Add real API calls. Replace the mock weather logic with a call to OpenWeatherMap, WeatherAPI, or any HTTP service. The tool handler is a normal Python function. Use httpx, requests, or any HTTP client you like.
Add more tools. Register as many tools as you want with additional @mcp.tool() decorators. Group related tools in one server rather than running separate servers for each.
Expose resources. MCP servers can expose read-only data the model can pull in as context. Use the @mcp.resource() decorator:
@mcp.resource("config://app")
def get_config() -> str:
"""Return current application configuration."""
return json.dumps({"version": "1.0", "debug": False})
Define prompts. Use @mcp.prompt() to register reusable prompt templates that the model can invoke with parameters.
Switch transport for remote deployment. Stdio works for local use. For serving over the network, pass transport="streamable-http" to mcp.run(). This lets remote clients connect over HTTP.
FAQ
Do I need uv, or can I use pip?
You can use pip. Install with pip install "mcp[cli]" and run the server with python server.py directly. The SDK does not depend on uv at runtime. uv is recommended because it handles virtual environments and dependency resolution faster, and the mcp install command works best with uv-managed projects.
Can I use async tool handlers?
Yes. FastMCP supports both sync and async functions. If your tool needs to make network calls or do I/O, define it as async def and the server will handle it correctly:
@mcp.tool()
async def fetch_data(url: str) -> str:
"""Fetch data from a URL."""
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.text
How do I debug when the tool is not showing up in Claude Desktop?
Check Claude Desktop’s MCP logs. On macOS, look in ~/Library/Logs/Claude/ for files named mcp*.log. These show connection attempts, server errors, and protocol messages. Running uv run mcp dev server.py in the inspector is the fastest way to verify your server works independently of Claude Desktop.
What is the difference between this and the Node.js approach?
The Python SDK uses decorators and type hints to define tools, while the Node.js SDK uses explicit method calls with Zod schemas. Both produce the same MCP protocol messages. The Python version tends to be more concise. If you already have a Node.js MCP server, both can run side by side in the same Claude Desktop config.