Build Your First MCP Server in Python with FastMCP

A
Author
03 Nov, 2026 4 min read 0 views

The official MCP Python SDK includes a high-level class called FastMCP that turns ordinary Python functions into MCP tools with a decorator. In this tutorial you will install the SDK, write a small server with two tools, run it and test it with the MCP Inspector.

Install the SDK

The package is called mcp. A common way to manage a project is with the uv tool, but pip also works. The cli extra adds the mcp command-line helper.

mkdir temp-server && cd temp-server
python -m venv .venv
source .venv/bin/activate
pip install "mcp[cli]"

Write the server

Create server.py. This server converts between Celsius and Fahrenheit.

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("temperature")


@mcp.tool()
def celsius_to_fahrenheit(celsius: float) -> float:
    """Convert a temperature from degrees Celsius to degrees Fahrenheit."""
    return celsius * 9 / 5 + 32


@mcp.tool()
def fahrenheit_to_celsius(fahrenheit: float) -> float:
    """Convert a temperature from degrees Fahrenheit to degrees Celsius."""
    return (fahrenheit - 32) * 5 / 9


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

How FastMCP builds the tool

When you apply @mcp.tool(), FastMCP reads the function and builds the MCP tool definition from it:

  • The name is the function name.
  • The description is the docstring. Write it for the model, not only for human maintainers.
  • The input schema is generated from the type hints. Here it says there is one required number argument.
  • Incoming arguments are validated against that schema before your function runs.

The return value is converted to MCP content automatically. A string becomes a text item; other values are serialized to text for the client. For precise control over the result shape, you can return content objects yourself, which a later post covers.

mcp.run() with no arguments starts the server on the stdio transport. This is what local clients expect.

Run it with the Inspector

The SDK's command-line helper can start the Inspector and your server together:

mcp dev server.py

If you prefer, you can start the Inspector yourself with npx @modelcontextprotocol/inspector python server.py. Either way a browser page opens. Click Connect, go to the Tools tab and list the tools. Run celsius_to_fahrenheit with 100 and you should get 212.0.

Add a tool with more structure

Type hints can describe richer inputs. You can use Annotated with a Pydantic Field to add per-argument descriptions that appear in the schema, and a default value makes an argument optional.

from typing import Annotated
from pydantic import Field
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("text-tools")


@mcp.tool()
def truncate(
    text: Annotated[str, Field(description="The text to shorten")],
    max_chars: Annotated[int, Field(description="Maximum length", ge=1)] = 80,
) -> str:
    """Shorten text to at most max_chars characters, adding an ellipsis if cut."""
    if len(text) <= max_chars:
        return text
    return text[: max_chars - 1] + "\u2026"


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

The ge=1 constraint becomes a minimum in the JSON Schema, so a call with max_chars=0 is rejected before your code runs.

Do not print to stdout

With stdio, standard output is the protocol channel. A stray print() call writes into it and the client will fail to parse the message. For debugging, write to stderr or use the logging module, which writes to stderr by default.

import sys

print("debug info", file=sys.stderr)

Add a resource and a prompt

FastMCP uses decorators for the other primitives too. A resource is a function tied to a URI, and a prompt is a function that returns text for the model. Add these to the same file, before the mcp.run() call.

@mcp.resource("docs://units")
def units_help() -> str:
    """Explain the units this server supports."""
    return "Supported units: Celsius (C) and Fahrenheit (F)."


@mcp.prompt()
def convert_prompt(value: str) -> str:
    """Ask the model to convert a temperature and explain it."""
    return "Convert " + value + " to the other unit and explain the formula."

Restart the Inspector. The Resources tab now lists docs://units, and the Prompts tab lists convert_prompt with a single argument called value. The SDK derived the argument from the function signature, in the same way it derived tool inputs.

Async tools

Tools can be async def functions, which is the right choice whenever you call a network API or a database driver with async support. The server runs on an event loop, so a blocking call inside a tool, such as a long requests.get, stalls every other request. Use an async HTTP client like httpx or move blocking work to a thread.

Common mistakes

  • Missing type hints, which gives the model a vague schema.
  • Writing no docstring, so the tool has no description and the model guesses when to use it.
  • Running the file with a different Python than the one where mcp is installed. Clients that launch your server need the full path to the right interpreter, which a later post on client configuration covers.

You now have a server that any MCP client can launch. Next, learn what happens on the wire when a client connects.

Was this article helpful?

Comments

No comments yet. Have a question or a tip? Be the first to share it.

Leave a comment

Comments are reviewed before they appear.