# Build an MCP server in Python with the official SDK

> To build an MCP server in Python, install the official `mcp` package with uv, create an `MCPServer`, and register each function as a tool with `@mcp.tool()`. The SDK writes the tool's schema from the function's type hints and docstring. Run the file over stdio with `mcp.run(transport="stdio")`, and a host such as Claude Desktop can start it and call the tools.

Mindket editors · Published 2026-11-04 · Updated 2026-11-05 · https://mindket.com/learn/mcp/build-mcp-server-python

## In short

- You will write a weather server with two tools, `get_alerts` and `get_forecast`, in a single Python file.
- The SDK builds each tool's schema from the function's type hints and docstring, so you never write JSON Schema by hand.
- A stdio server must not use `print()`. Anything on stdout breaks the protocol, so log to stderr.
- SDK 2.x renamed `FastMCP` to `MCPServer`, so an older tutorial that imports `FastMCP` will not match.

## Before you start

This lesson builds the weather server from the official [MCP build guide](https://modelcontextprotocol.io/docs/develop/build-server). It is the server most Python tutorials start from, and it has every part a first server needs: a server object, two tools, a docstring that becomes the schema and the stdio transport.

One trap comes before any code. Version 2 of the Python SDK renamed `FastMCP` to `MCPServer`, and the old `mcp.server.fastmcp` import path is gone. A tutorial that still uses it targets the 1.x line. `pip install mcp` now installs 2.x, and the code below is for 2.x.

- Python 3.11 or newer.
- `uv`, the Python package manager the guide uses. The README says plain `pip install "mcp[cli]"` works too.
- A patched Node.js, but only for the Inspector in the last step: 22.23.4 or later on the 22 line, 24.22.1 or later on the 24 line, or 26.12.1 and up. [Lesson 3](https://mindket.com/learn/mcp/mcp-inspector) covers it.
- An internet connection. The tools call the US National Weather Service API, which covers US locations only.
- Claude Desktop, if you want to talk to the finished server. Any MCP host will do.

**Python SDK v2, from the first beta**

1. **June 30, 2026** First v2 beta. Version 2.0.0b1 arrived with full support for the 2026-07-28 spec.
2. **July 28, 2026** Version 2.0.0. The stable v2 came out the day the spec did, with `MCPServer` in place of `FastMCP`.
3. **August 24, 2026** Version 2.1.0. The `Client` class accepts `StdioServerParameters`, and prompt messages can carry images and audio.
4. **September 7, 2026** Version 2.2.0. The HTTP client follows redirects only inside the endpoint's origin, and idle Streamable HTTP sessions expire after half an hour. Version 1.30.0 got the same changes.
5. **October 2, 2026** Version 2.3.0. Mostly fixes, plus three new options.

Sources: [MCP Python SDK release v2.0.0b1](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.0.0b1) (read 2026-11-05), [MCP Python SDK release v2.0.0](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.0.0) (read 2026-11-05), [MCP Python SDK release v2.1.0](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.1.0) (read 2026-11-05), [MCP Python SDK release v2.2.0](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.2.0) (read 2026-11-05), [MCP Python SDK release v2.3.0](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.3.0) (read 2026-11-05)

## Build the server in seven steps

The guide gives the code in four pieces: the imports, two helper functions, the two tools and the run call. Pasted in that order they make one file, `weather.py`.

````python
from typing import Any

import httpx2
from mcp.server import MCPServer

# Initialize MCPServer
mcp = MCPServer("weather")

# Constants
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"


async def make_nws_request(url: str) -> dict[str, Any] | None:
    """Make a request to the NWS API with proper error handling."""
    headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"}
    async with httpx2.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


def format_alert(feature: dict) -> str:
    """Format an alert feature into a readable string."""
    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")}
"""


@mcp.tool()
async def get_alerts(state: str) -> str:
    """Get weather alerts for a US state.

    Args:
        state: Two-letter US state code (e.g. CA, 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."

    alerts = [format_alert(feature) for feature in data["features"]]
    return "\n---\n".join(alerts)


@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get weather forecast for a location.

    Args:
        latitude: Latitude of the location
        longitude: Longitude of the location
    """
    # First get the forecast grid endpoint
    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."

    # Get the forecast URL from the points response
    forecast_url = points_data["properties"]["forecast"]
    forecast_data = await make_nws_request(forecast_url)

    if not forecast_data:
        return "Unable to fetch detailed forecast."

    # Format the periods into a readable forecast
    periods = forecast_data["properties"]["periods"]
    forecasts = []
    for period in periods[:5]:  # Only show next 5 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)


if __name__ == "__main__":
    mcp.run(transport="stdio")

````

1. Install [uv](https://docs.astral.sh/uv/). On macOS or Linux run `curl -LsSf https://astral.sh/uv/install.sh | sh`. On Windows, run `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`. Then restart the terminal so the `uv` command is found.
2. Create the project with `uv init weather`, then move into it with `cd weather`.
3. Create and activate a virtual environment. Run `uv venv`, then `source .venv/bin/activate` on macOS and Linux or `.venv\Scripts\activate` on Windows.
4. Install the SDK with `uv add "mcp[cli]"`. For Windows the guide writes it without the quotes: `uv add mcp[cli]`.
5. Create `weather.py` and paste in the code above.
6. Run `uv run weather.py`. It prints nothing and waits, because it is listening on stdin for a host. Stop it with Ctrl+C.
7. Open it in the Inspector with `uv run mcp dev weather.py`, and call `get_alerts` with a two-letter state code such as `CA`.

## What each part of the file does

The decorator `@mcp.tool()` turns an ordinary function into a tool a model can call. The SDK reads the function itself. Its name becomes the tool's name, its docstring becomes the description the model reads, and its type hints become the input schema. Write the docstring for the model, because the model is the one who reads it.

The README shows the same idea in 15 lines: one `add` tool and one greeting resource. If the weather server looks long, start there. Most of the extra length is the helper that calls the weather API, and none of it is MCP.

The tools here use `async def` because they wait on a web request.

| In the file | What it does | What the host sees |
| --- | --- | --- |
| `mcp = MCPServer("weather")` | Creates the server object and names it | A server called weather |
| `@mcp.tool()` on `get_alerts` | Registers the function as a tool | A tool with one text input, `state` |
| `@mcp.tool()` on `get_forecast` | Registers a second tool | A tool with two number inputs, `latitude` and `longitude` |
| `mcp.run(transport="stdio")` | Reads requests on stdin and answers on stdout | A local process the host starts and stops |

## Why print() breaks a stdio server

Over stdio, standard output carries the protocol itself. Every line the server writes there has to be a JSON-RPC message, so one stray `print()` corrupts the stream and the host drops the connection. The guide says to keep `print()` out of a stdio server entirely.

Use the standard `logging` module, which writes to stderr, with one logger per module. Over HTTP the rule does not apply, because standard output does not touch the HTTP responses.

````python
import logging

logger = logging.getLogger(__name__)

# Bad (stdio)
print("Processing request")

# Good (stdio)
logger.info("Processing request")  # writes to stderr

````

## Run it in Claude Desktop

The guide tests the server in Claude for Desktop. Open `claude_desktop_config.json`, add the server under `mcpServers`, save the file and restart the app. Below is the guide's entry for macOS and Linux, and its Windows form writes the path with double backslashes, such as `C:\ABSOLUTE\PATH\TO\PARENT\FOLDER\weather`.

The path to the project folder must be absolute, and you may need the full path to `uv` in the `command` field. Run `which uv` or `where uv` to print it. Then quit Claude Desktop completely and open it again. Lesson 4 shows this app and two others side by side.

````json
{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "--directory",
        "/ABSOLUTE/PATH/TO/PARENT/FOLDER/weather",
        "run",
        "weather.py"
      ]
    }
  }
}

````

## What MCP's co-creator and FastMCP's creator say about Python SDK v2

David Soria Parra, a member of technical staff at Anthropic and co-creator of MCP, [posted on X](https://x.com/dsp_/status/2072307451982958925) on 1 July 2026 that the first v2 betas of the Python and TypeScript SDKs were out. He asked builders to test them and report on ergonomics before the stateless spec landed on 28 July. Most tutorials from before that date target 1.x.

Jeremiah Lowin, founder and CEO of Prefect and creator of FastMCP, [posted on X](https://x.com/jlowin/status/2082217726949445778) on release day that FastMCP v4 is rebuilt on the v2 Python SDK. That FastMCP is a separate package: `uv add fastmcp`, then `from fastmcp import FastMCP`. Its [welcome page](https://gofastmcp.com/getting-started/welcome) says it created the high-level API the official SDK took in during 2024. A tutorial that imports `fastmcp` targets that package, and one that imports `mcp.server.fastmcp` is SDK 1.x code.

The old import tripped up enough people that version 2.1.1 now points it at the [migration guide](https://py.sdk.modelcontextprotocol.io/migration/). The maintainers backported the warning to 2.0.1 a day later, on 26 August, because a lot of people were running into the error. Port a 1.x server with that guide open.

> FastMCP v4 is totally rebuilt on top of the v2 Python SDK
>
> Jeremiah Lowin, Founder and CEO of Prefect, creator of FastMCP, [X](https://x.com/jlowin/status/2082217726949445778) (read 2026-11-05)

[![David Soria Parra (@dsp_) on X, 1 July 2026. Ahead of the new, stateless MCP specification coming on July 28th, the team was hard at work releasing the first betas of the Typescript SDK v2.0.0 and Python SDK v2.0.0. This should make building MCP Servers and clients much easier.](https://mindket.com/blog/shots/x-dsp-sdk-v2-betas.webp?v=030329044878)](https://x.com/dsp_/status/2072307451982958925)

David Soria Parra on X, 1 July 2026

[![Jeremiah Lowin (@jlowin) on X, 28 July 2026. The new spec is out, along with the FastMCP v4 beta. FastMCP v4 is totally rebuilt on top of the v2 Python SDK, which means you get all the fun of the new protocol with the same old FastMCP form you know and love.](https://mindket.com/blog/shots/x-jlowin-fastmcp-v4-sdk-v2.webp?v=cfa8aba198d6)](https://x.com/jlowin/status/2082217726949445778)

Jeremiah Lowin on X, 28 July 2026

> Screenshots of X's own embeds of the posts, taken on 5 November 2026, with the time in UTC. X cuts both posts short with Show more. Select one to open the post on X.

## Common mistakes

- Importing `FastMCP` from `mcp.server.fastmcp`. That path is gone in 2.x. Use `from mcp.server import MCPServer`.
- Calling `print()` in a stdio server. The host sees garbage on the wire and disconnects. Use `logging`.
- A relative path in the host's config. Hosts start the server from their own working directory, so `weather.py` alone fails. Use the absolute path.
- Editing the host's config and not restarting the host. Hosts read their config at launch.
- Asking for a forecast outside the US. The weather API behind the tools covers US locations only.
- Trusting the error message. The helper `make_nws_request` swallows every exception and returns `None`, so every network problem reads as "Unable to fetch alerts". Log the exception inside it while you debug.

## Try it yourself

Add the README's resource to `weather.py`, restart `uv run mcp dev weather.py` and look for it in the Inspector. Resources are the second thing a server can offer, next to tools.

````python
@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"

````

- It appears under resource templates, not under resources, because it takes a parameter.
- Read it with the name World and check that the reply is `Hello, World!`.
- Break the server on purpose with a `print()` and watch what the Inspector shows.

## Next: the same server in TypeScript

Lesson 2 builds a server with the [official TypeScript SDK](https://mindket.com/learn/mcp/build-mcp-server-typescript), and lesson 3 goes through the Inspector tab by tab. If the protocol itself is new to you, [what is MCP](https://mindket.com/blog/what-is-mcp) explains the architecture.

> A server you run locally can do whatever your user account can do. [Are MCP servers safe?](https://mindket.com/blog/are-mcp-servers-safe) covers what to check before you run one someone else wrote.

## Check yourself

1. Which import creates the server in the 2.x Python SDK?

   A. `from mcp.server.fastmcp import FastMCP`
   B. `from mcp.server import MCPServer`
   C. `from mcp import FastMCP`

   Answer: B. SDK 2.x renamed `FastMCP` to `MCPServer`, and the old `mcp.server.fastmcp` path is gone.

2. Which part of a tool function becomes the description a model reads?

   A. Its docstring
   B. Its name
   C. Its return type hint

   Answer: A. The name becomes the tool's name and the type hints become the input schema, so the docstring is the description and you write it for the model.

3. Why must a stdio server never call `print()`?

   A. `print()` is slower than `logging`
   B. The host cannot read Python output at all
   C. Standard output carries the protocol, so a stray line corrupts the stream

   Answer: C. Over stdio, standard output is the protocol itself, so you log with `logging`, which writes to stderr.

4. Every network problem in the weather server reads as "Unable to fetch alerts". Why?

   A. The host rewrites every error message
   B. The weather API needs a key that is missing
   C. Python 3.11 hides network errors
   D. The helper swallows every exception and returns `None`

   Answer: D. `make_nws_request` catches every exception, so log the exception inside it while you debug.

## Frequently asked questions

### Will the weather server run on Python 3.10?

Possibly. The build guide and the package's PyPI listing say 3.10 or higher, while the SDK's own docs ask for one version newer. The pages disagree, so use 3.11 for the lesson and treat 3.10 as unconfirmed.

### What else changed in the Python SDK between 1.x and 2.x?

The decorators `@mcp.tool()`, `@mcp.resource()` and `@mcp.prompt()` work as before. Transport options such as `host` and `port` moved from the constructor to `run()`, the HTTP client is now `httpx2`, the WebSocket transport is gone, and a new `Client` class sits on top of `ClientSession`. To follow an older tutorial, pin `mcp>=1.28,<2`.

### Do I need uv, or does pip work?

Pip works. The README gives `pip install "mcp[cli]"` as the alternative to `uv add "mcp[cli]"`. The official guide uses uv because it also runs the file with `uv run`, and the Claude Desktop config in the guide calls `uv` too.

### What does the cli part of mcp[cli] add?

It adds the `mcp` command with `mcp dev` to open a server in the Inspector, `mcp run` to run it and `mcp install` to register it with Claude Desktop. The extra installs `typer` and `python-dotenv`, and a deployed server may not need it.

### How do I run the server over HTTP instead of stdio?

Run `uv run mcp run server.py --transport streamable-http`. The README then connects a client at `http://localhost:8000/mcp`. Stdio fits a host that starts the server on your machine, and Streamable HTTP is the transport you deploy.

### Does the weather server need an API key?

No key appears in the guide's code. It sends a `User-Agent` header to the National Weather Service API and nothing else. That API covers US locations only.

### Can a tool be a plain function instead of async?

Yes. According to the SDK's v2 notes, a plain `def` tool, resource or prompt runs on a worker thread. Use `async def` when the tool waits on the network, as the weather tools do.

## Sources

- [MCP docs: Build an MCP server](https://modelcontextprotocol.io/docs/develop/build-server) (read 2026-11-04)
- [MCP Python SDK: README](https://github.com/modelcontextprotocol/python-sdk) (read 2026-11-04)
- [MCP Python SDK docs: Installation](https://py.sdk.modelcontextprotocol.io/get-started/installation/) (read 2026-11-04)
- [MCP Python SDK docs: First steps](https://py.sdk.modelcontextprotocol.io/get-started/first-steps/) (read 2026-11-04)
- [MCP Python SDK docs: Connect to a real host](https://py.sdk.modelcontextprotocol.io/get-started/real-host/) (read 2026-11-04)
- [MCP Python SDK docs: What's new in v2](https://py.sdk.modelcontextprotocol.io/whats-new/) (read 2026-11-04)
- [MCP docs: Connect to local MCP servers](https://modelcontextprotocol.io/docs/develop/connect-local-servers) (read 2026-11-04)
- [MCP Python SDK docs: Migration guide](https://py.sdk.modelcontextprotocol.io/migration/) (read 2026-11-05)
- [FastMCP: Welcome](https://gofastmcp.com/getting-started/welcome) (read 2026-11-05)
- [MCP Inspector: README](https://github.com/modelcontextprotocol/inspector) (read 2026-11-05)
- [MCP Python SDK release v2.0.0b1](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.0.0b1) (read 2026-11-05)
- [MCP Python SDK release v2.0.0](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.0.0) (read 2026-11-05)
- [MCP Python SDK release v2.0.1](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.0.1) (read 2026-11-05)
- [MCP Python SDK release v2.1.0](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.1.0) (read 2026-11-05)
- [MCP Python SDK release v2.1.1](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.1.1) (read 2026-11-05)
- [MCP Python SDK release v2.2.0](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.2.0) (read 2026-11-05)
- [MCP Python SDK release v2.3.0](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.3.0) (read 2026-11-05)
- [David Soria Parra on X: first betas of the Python and TypeScript SDK v2](https://x.com/dsp_/status/2072307451982958925) (read 2026-11-05)
- [Jeremiah Lowin on X: FastMCP v4 rebuilt on the v2 Python SDK](https://x.com/jlowin/status/2082217726949445778) (read 2026-11-05)

[Browse MCP servers](https://mindket.com/mcp-servers)
