Mindket

Build an MCP server in Python with the official SDK

A black banner titled "MCP course". Its line reads "Build an MCP server in Python or TypeScript, test it, connect it".

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.

In this lesson

  • 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.
All courses
On this page
  1. Before you start
  2. Build the server in seven steps
  3. What each part of the file does
  4. Why print() breaks a stdio server
  5. Run it in Claude Desktop
  6. What MCP's co-creator and FastMCP's creator say about Python SDK v2
  7. Common mistakes
  8. Try it yourself
  9. Next: the same server in TypeScript
  10. Check yourself
  11. Frequently asked questions
  12. Sources

Before you start

This lesson builds the weather server from the official MCP build guide. 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 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. First v2 beta

    Version 2.0.0b1 arrived with full support for the 2026-07-28 spec.

    Source: MCP Python SDK release v2.0.0b1 (read November 5, 2026)

  2. Version 2.0.0

    The stable v2 came out the day the spec did, with MCPServer in place of FastMCP.

    Source: MCP Python SDK release v2.0.0 (read November 5, 2026)

  3. Version 2.1.0

    The Client class accepts StdioServerParameters, and prompt messages can carry images and audio.

    Source: MCP Python SDK release v2.1.0 (read November 5, 2026)

  4. 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.

    Source: MCP Python SDK release v2.2.0 (read November 5, 2026)

  5. Version 2.3.0

    Mostly fixes, plus three new options.

    Source: MCP Python SDK release v2.3.0 (read November 5, 2026)

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.

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. 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.

The four lines of weather.py that make it an MCP server
In the fileWhat it doesWhat the host sees
mcp = MCPServer("weather")Creates the server object and names itA server called weather
@mcp.tool() on get_alertsRegisters the function as a toolA tool with one text input, state
@mcp.tool() on get_forecastRegisters a second toolA tool with two number inputs, latitude and longitude
mcp.run(transport="stdio")Reads requests on stdin and answers on stdoutA 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.

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.

{
  "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 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 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 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. 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 (read November 5, 2026)
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.
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.
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.

@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, and lesson 3 goes through the Inspector tab by tab. If the protocol itself is new to you, what is MCP explains the architecture.

A server you run locally can do whatever your user account can do. 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?

    Show answer

    B. from mcp.server import MCPServer 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?

    Show answer

    A. Its docstring 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()?

    Show answer

    C. Standard output carries the protocol, so a stray line corrupts the stream 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?

    Show answer

    D. The helper swallows every exception and returns None 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

  1. MCP docs: Build an MCP server (read November 4, 2026)https://modelcontextprotocol.io/docs/develop/build-server
  2. MCP Python SDK: README (read November 4, 2026)https://github.com/modelcontextprotocol/python-sdk
  3. MCP Python SDK docs: Installation (read November 4, 2026)https://py.sdk.modelcontextprotocol.io/get-started/installation/
  4. MCP Python SDK docs: First steps (read November 4, 2026)https://py.sdk.modelcontextprotocol.io/get-started/first-steps/
  5. MCP Python SDK docs: Connect to a real host (read November 4, 2026)https://py.sdk.modelcontextprotocol.io/get-started/real-host/
  6. MCP Python SDK docs: What's new in v2 (read November 4, 2026)https://py.sdk.modelcontextprotocol.io/whats-new/
  7. MCP docs: Connect to local MCP servers (read November 4, 2026)https://modelcontextprotocol.io/docs/develop/connect-local-servers
  8. MCP Python SDK docs: Migration guide (read November 5, 2026)https://py.sdk.modelcontextprotocol.io/migration/
  9. FastMCP: Welcome (read November 5, 2026)https://gofastmcp.com/getting-started/welcome
  10. MCP Inspector: README (read November 5, 2026)https://github.com/modelcontextprotocol/inspector
  11. MCP Python SDK release v2.0.0b1 (read November 5, 2026)https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.0.0b1
  12. MCP Python SDK release v2.0.0 (read November 5, 2026)https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.0.0
  13. MCP Python SDK release v2.0.1 (read November 5, 2026)https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.0.1
  14. MCP Python SDK release v2.1.0 (read November 5, 2026)https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.1.0
  15. MCP Python SDK release v2.1.1 (read November 5, 2026)https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.1.1
  16. MCP Python SDK release v2.2.0 (read November 5, 2026)https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.2.0
  17. MCP Python SDK release v2.3.0 (read November 5, 2026)https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.3.0
  18. David Soria Parra on X: first betas of the Python and TypeScript SDK v2 (read November 5, 2026)https://x.com/dsp_/status/2072307451982958925
  19. Jeremiah Lowin on X: FastMCP v4 rebuilt on the v2 Python SDK (read November 5, 2026)https://x.com/jlowin/status/2082217726949445778

Share this lesson

See what other MCP servers can do

Browse the MCP servers on Mindket and compare them with the one you just built.

Browse MCP servers

This lesson as Markdown · MCP course: build, test and connect MCP servers · All courses