To build an MCP server in Python, install the official SDK with uv add "mcp[cli]", create a FastMCP instance, decorate ordinary Python functions with @mcp.tool() so their type hints and docstrings become the tool schema, and start it with mcp.run(transport="stdio"). That gives you a working server an MCP host like Claude for Desktop can launch and call. This guide walks the full path: a minimal server first, a real API-backed tool second, then testing and connecting it to a host.
Key takeaways
- The short answer: install
mcp[cli], wrap functions in@mcp.tool(), and runmcp.run(transport="stdio"). - FastMCP reads your function's type hints and docstring to auto-generate the tool's input schema and description, so you write plain Python, not JSON schema by hand.
- For stdio servers, never write to stdout with
print(). It corrupts the JSON-RPC stream and breaks the server. Log to stderr instead. - Test before wiring anything up:
npx @modelcontextprotocol/inspectorruns an interactive UI against your server. - You need Python 3.10 or higher and MCP Python SDK 1.2.0 or higher.
- To use the server in a host, add it under the
mcpServerskey of the host's config with an absolute path, then restart the host.
What you need
- Python 3.10 or higher.
- The uv package manager (recommended by the MCP docs). Pip works too if you prefer it.
- The MCP Python SDK, version 1.2.0 or higher, published on PyPI as
mcp. - An MCP host to consume the server, such as Claude for Desktop. The server itself runs anywhere Python does.
Build and run an MCP server in Python
Follow these steps in order. By the end you will have a server running over stdio that a host can call.
- Create the project and install the SDK. Using uv:
If you use pip instead, the equivalent isuv init weather cd weather uv venv source .venv/bin/activate uv add "mcp[cli]" httpx touch weather.pypython -m venv .venv && source .venv/bin/activatefollowed bypip install "mcp[cli]" httpx. - Write a minimal server to confirm the wiring. Put this in
weather.py. It exposes one tool and runs over stdio:
The string passed tofrom mcp.server.fastmcp import FastMCP 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(transport="stdio")FastMCPnames the server. The decorator turnsaddinto a callable tool; its signature becomes the input schema and its docstring becomes the description the model sees. - Run it. Start the server with:
A stdio server does not print a banner. It sits waiting for JSON-RPC messages on standard input, which is correct. To interact with it, use the Inspector in the next step rather than typing into the terminal.uv run weather.py - Test it with the MCP Inspector. In a second terminal, from the project folder, run:
This opens a browser UI. Open the Tools tab, selectnpx @modelcontextprotocol/inspector uv --directory . run weather.pyadd, pass two numbers, and confirm you get the sum back. This proves the server speaks MCP correctly before you connect any host. - Add a real, useful tool. Replace the
addstub with a tool that calls an external API. This one queries the US National Weather Service for active alerts:
Note that tools can befrom typing import Any import httpx from mcp.server.fastmcp import FastMCP mcp = FastMCP("weather") NWS_API_BASE = "https://api.weather.gov" USER_AGENT = "weather-app/1.0" 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: r = await client.get(url, headers=headers, timeout=30.0) r.raise_for_status() return r.json() except Exception: return None @mcp.tool() async def get_alerts(state: str) -> str: """Get active weather alerts for a US state. Args: state: Two-letter US state code, for example CA or NY. """ url = f"{NWS_API_BASE}/alerts/active/area/{state}" 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 "No active alerts for this state." events = [f.get("properties", {}).get("event", "Unknown") for f in data["features"]] return "\n".join(events) if __name__ == "__main__": mcp.run(transport="stdio")async. FastMCP handles both sync and async functions. TheArgs:block in the docstring documents each parameter for the model. - Connect the server to a host. For Claude for Desktop, open its config file (create it if it does not exist) at
~/Library/Application Support/Claude/claude_desktop_config.jsonon macOS, or%AppData%\Claude\claude_desktop_config.jsonon Windows, and add your server undermcpServers:
Use the absolute path to the project folder. Get it with{ "mcpServers": { "weather": { "command": "uv", "args": ["--directory", "/ABSOLUTE/PATH/TO/weather", "run", "weather.py"] } } }pwd. Save the file and restart the host completely. The server's tools then appear in the host and the model can call them with your approval.
How FastMCP turns functions into tools
The reason this is so short is that FastMCP does the schema work for you. When you decorate a function with @mcp.tool(), it inspects the signature and builds the tool's JSON schema from your type hints, uses the docstring as the tool description, and marks parameters without defaults as required. This is why clear type hints and a precise docstring matter more than usual: they are the interface the model reasons over when it decides whether and how to call your tool.
Tools are one of three capabilities a server can expose. Resources are read-only, file-like data the client can load into context. Prompts are reusable templates a user can invoke. Most servers start with tools because tools are the actions a model takes. The server concepts reference covers resources and prompts in depth once you need them.
Sync versus async, and when to reach for HTTP transport
Use a plain def for fast, in-process work and async def for anything that waits on a network or disk, so a slow call does not block the server. The examples above use stdio transport, which is right when the host launches the server as a local subprocess. If you want to host the server as a standalone service that multiple clients reach over the network, run it with an HTTP transport instead. Unlike stdio, HTTP transports allow normal stdout logging because output does not share a channel with the protocol.
Troubleshooting
The failures below account for most first-run problems.
- The server connects but immediately disconnects, or the host shows a JSON parse error. Something wrote to stdout. In a stdio server, a stray
print(), a library banner, or a debug statement corrupts the JSON-RPC stream. Route all logging to stderr, for exampleprint("msg", file=sys.stderr)or the standardloggingmodule, which writes to stderr by default. - The host cannot start the server. The
commandin the config is usually the culprit. The host does not inherit your shell PATH, souvmay not be found. Put the absolute path to the executable incommand(find it withwhich uv), and make sure the--directorypath is absolute, not relative. - Tools do not appear in the host. Confirm the config is valid JSON, that the server is nested under the
mcpServerskey, and that you fully restarted the host after saving. A running host does not pick up config changes live. - The model calls the tool with the wrong arguments. Tighten the type hints and rewrite the docstring. The model only knows what the generated schema and description tell it, so vague names and missing
Args:lines lead to bad calls. - Import error on
mcp.server.fastmcp. Your SDK is too old. Upgrade to 1.2.0 or higher withuv add "mcp[cli]"orpip install --upgrade "mcp[cli]".
What to do next
- Read the official Build an MCP server tutorial for the full weather example, including a second forecast tool.
- Add resources and prompts using the server concepts reference once tools alone are not enough.
- Keep the MCP Inspector in your loop: change code, rebuild, reconnect, retest. It is faster than debugging through a host.
- Study the Python SDK repository for lower-level control when FastMCP's defaults are not enough.
- Review the host-side setup in connect local servers if the config step gives you trouble.
If you are building servers to feed an agent loop, see my notes on agentic orchestration with the Anthropic APIs and on what MCP means for enterprise AI. If your tools wrap a local model rather than a remote API, running LLMs locally with Ollama pairs naturally with an MCP server.