Build Your First MCP Server in Python with FastMCP
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
mcpis 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.
Comments
No comments yet. Have a question or a tip? Be the first to share it.